티스토리 뷰

728x90
반응형

지난 글에선 Node.js의 용어 정리와 db.ts, User.ts, Archive.ts 세 모듈에 대해서 알아봤다.

 

이번 글에선 남은 모듈을 최대한 정리하고,  이어지는 구현은 손으로 따라가면서 익힐 예정이다.

 

참고로 지난 글보다 모듈이 하나 늘어서, 패키지 구조는 아래와 같다.

우선 utils 폴더를 보고, 그다음으로 routes, middleware, controller 순으로 올라갈 예정이다.

 

시작!

 

/src/utils

 

지난 글에 적은 대로, utils는 재사용 가능한 유틸리티 모듈이 모여있는 폴더이다.

 

모듈 이름만 봐도 여러 곳에서 많이 쓰일 것 같은데, 하나씩 알아보자.

 

generateToken.ts

 

import jwt from 'jsonwebtoken';

const generateToken = (id: string) => {
  return jwt.sign({ id }, process.env.JWT_SECRET!, {
    expiresIn: '30d',
  });
};

export default generateToken;

이 모듈은 JWT를 사용해서 토큰을 생성하는 함수를 정의하고 있다.

import jwt from 'jsonwebtoken';

jsonwebtoken 패키지를 가져온다. 이 패키지를 JWT 생성과 검증 기능을 제공한다.

const generateToken = (id: string) => {
  return jwt.sign({ id }, process.env.JWT_SECRET!, {
    expiresIn: '30d',
  });
};

역시 const를 이용해 generateToken 메서드를 정의한다.

 

해당 함수는 문자열 타입인 id를 입력받아 JWT를 생성한다.

 

계속해서 jwt.sign() 메서드를 호출해서 JWT를 생성한다. 이 메서드는 세 개의 매개변수를 갖는다.

 

  • { id }
    첫 번째 매개변수는 위에서 입력받은 아이디로, 토큰에 포함될 정보를 제공하는 객체이다.
    이 경우, 토큰은 사용자의 ID 정보를 포함하게 된다.
  • process.env.JWT_SECRET!
    두 번째 매개변수는 서명을 생성하는 데 사용되는 시크릿 키이다. 이 키는 환경 변수에서 가져오며, 개발자가 임의로 입력할 수 있다.
    느낌표(!)는 전에 봤듯이, JWT_SECRET 환경변수 값이 null 또는 undefined가 아님을 명시한다.
  • { expiresIn: '30d' }
    세 번째 매개변수는 옵션을 설정하는 객체이다. 이 경우, 토큰의 유효기간을 30일로 설정한다.
export default generateToken;

끝으로 generateToken을 default로 내보내 다른 모듈에서 사용할 수 있도록 한다.

 

httpRequest.ts

 

import express from 'express';
import asyncHandler from 'express-async-handler';

const keyCopy = asyncHandler(async (req: express.Request, res: express.Response) => {
    const { email, password } = req.body;
});

const subCopy = asyncHandler(async (req: express.Request, res: express.Response) => {
    const { email, password } = req.body;
});

const keyImage = asyncHandler(async (req: express.Request, res: express.Response) => {
    const { email, password } = req.body;
});

export {
    keyCopy,
    subCopy,
    keyImage
};

이 모듈은 Express를 이용해 서버에서 사용할 HTTP 요청 핸들러 메서드를 세 개 정의하고 있다.

 

각 함수는 비동기 함수이다.

import express from 'express';
import asyncHandler from 'express-async-handler';

express 프레임워크에서 제공하는 express 모듈을 가져온다. 이는 웹 앱을 만들기 위한 프레임워크이다.

 

아래에선 express-async-handler패키지에서 asyncHandler 모듈을 가져온다.

 

이는 Express 미들웨어에서 발생하는 비동기 예외를 감지하고 처리하는 기능을 제공한다.

const keyCopy = asyncHandler(async (req: express.Request, res: express.Response) => {
    const { email, password } = req.body;
});

keyCopy라는 이름의 비동기 핸들러 메서드를 정의한다.

 

해당 함수는 클라이언트에서 보내는 HTTP 요청을 처리한다.

 

여기서 req에는 클라이언트로부터 받은 정보가 들어있고, res에는 클라이언트로 보낼 정보를 담을 수 있다.

 

요청 바디(req.body)에서 이메일과 전화번호를 추출한다.

const subCopy = asyncHandler(async (req: express.Request, res: express.Response) => {
    const { email, password } = req.body;
});

비슷한 내용의 핸들러를 한 번 더 정의한다. 이유는 아직 모르겠다.

const keyImage = asyncHandler(async (req: express.Request, res: express.Response) => {
    const { email, password } = req.body;
});

마찬가지 핸들러 메서드이다. 세 개의 내부 로직이 아직 완성되지 않은 것으로 보인다.

export {
    keyCopy,
    subCopy,
    keyImage
};

세 개의 핸들러 메서드를 모듈 밖으로 내보낸다. 다른 모듈에서 이 함수들을 가져와 사용할 수 있다.

 

export default vs. export

 

  • export default
    모듈에서 단 하나의 항목(함수, 객체, 변수)만 내보낼 때 사용한다. 다른 모듈에서 해당 항목의 이름을 마음대로 붙여도 되며,
    import시 가져온 항목을 중괄호({})로 감싸지 않아야 한다.
  • export
    모듈에서 여러 항목을 내보낼 때 사용한다. 다른 모듈에서 가져올 시 명시적인 이름(위의 예에선 keyCopy)을 통해 가져와야 하며,
    import시 가져온 항목을 중괄호({})로 감싸야한다.

 

/src/routes

 

이전 글에 적었듯이, routes 폴더는 웹플럭스의 그것과 비슷하게 요청을 받아 엔드포인트로 라우팅 해주는 역할을 한다.

 

관련된 엔드포인트를 한 곳에 모아 관리하기 때문에 관리가 쉽고 가독성이 뛰어나다.

 

copyRoutes.ts

 

import express from 'express';
import { keyCopy, subCopy, keyImage } from '../utils/httpRequest';

const router = express.Router();

router.route('/keyCopy').post(keyCopy);
router.route('/subCopy').post(subCopy);
router.route('/keyImage').post(keyImage);

export default router;

이 모듈은 위의 httpRequest 모듈에서 생성한 함수를 가져와 라우팅 하는 역할을 한다.

import express from 'express';
import { keyCopy, subCopy, keyImage } from '../utils/httpRequest';

express모듈을 가져오고, 위에서 생성한 keyCopy, subCopy, keyImage 메서드를 httpRequest 모듈에서 가져온다.

const router = express.Router();

express 프레임워크에서 제공하는 Router 객체를 생성해 router에 할당한다.

 

Router 객체는 라우트를 그룹화, 모듈화 해서 프로젝트 구조를 잡는데 유용하다.

router.route('/keyCopy').post(keyCopy);
router.route('/subCopy').post(subCopy);
router.route('/keyImage').post(keyImage);

router.route 메서드를 사용해서 URL 경로와 HTTP 메서드를 연결해 준다.

 

예를 들어 '/keyCopy'라는 URL에 대한 POST 요청은 keyCopy 함수에게 라우팅 된다.

export default router;

마지막으로 router 객체를 export default로 내보낸다.

 

userRoutes.ts

 

import express from 'express';
import { registerUser, authUser } from '../controllers/userController';

const router = express.Router();

router.route('/').post(registerUser);
router.route('/login').post(authUser);

export default router;

이 모듈은 user와 연관된 route를 생성하는 역할을 한다.

import express from 'express';
import { registerUser, authUser } from '../controllers/userController';

express와 { registerUser, authUser }를 잠시 후 알아보게 될 userController 모듈에서 가져온다.

 

중괄호로 감싸진 것을 보니 해당 모듈에서 export를 이용해 내보낸 것을 추측할 수 있다.

const router = express.Router();

위의 경우와 마찬가지로 express 프레임워크에서 제공하는 Router 메서드를 가져와 router에 할당한다.

router.route('/').post(registerUser);
router.route('/login').post(authUser);

두 개의 라우터를 설정한다.

 

'/'로의 POST 요청은 registerUser로,

'/login'로의 POST 요청은 authUser로 전달 및 실행된다.

export default router;

마지막으로 export default를 이용해 router를 내보내준다.

 

/src/middleware

 

이 폴더는 미들웨어, 그러니까 컴포넌트 사이에서 특정 역할을 하는 모듈을 모아놓는 곳이다.

 

주로 HTTP 요청과 응답, 그리고 다른 미들웨어를 호출하는 함수를 매개변수로 가지며,

로깅, 에러 핸들링, 보안, 인증, 요청 검증, CORS 설정 등의 기능을 수행한다.

 

auth.ts

import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';

interface TokenPayload {
  id: string;
  iat: number;
  exp: number;
}

declare global {
  namespace Express {
    interface Request {
      userId?: string;
    }
  }
}

const authMiddleware = (
  req: Request, res: Response, next: NextFunction
) => {
  const { authorization } = req.headers;

  if (!authorization) {
    return res.status(401).json({ error: 'No token provided' });
  }

  const token = authorization.replace('Bearer ', '').trim();

  try {
    const data = jwt.verify(token, process.env.JWT_SECRET_KEY || '');

    const { id } = data as TokenPayload;

    req.userId = id;

    return next();
  } catch {
    return res.status(401).json({ error: 'Invalid token' });
  }
};

export default authMiddleware;

코드가 길지만 요약하면, 이 모듈은 JWT를 사용해 인증을 처리하는 미들웨어이다.

 

요청 헤더에 담긴 토큰을 검증하고, 유효하다면 요청 객체에 사용자 ID를 첨부하고

 

그렇지 않다면 에러메시지와 401 UNAUTHORIZED 상태 코드를 반환한다.

import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';

위 코드는 express 패키지에서 Request, Response, NextFunction을 가져온다.

 

해당 항목들은 Express 미들웨어에서 사용되는 객체와 함수이다.

 

바로 아래에선 아까도 등장했던 jsonwebtoken 패키지에서 jwt를 가져온다.

 

위에선 이를 사용해 JWT를 발급했다면, 여기서는 JWT를 검증하는 역할을 한다.

interface TokenPayload {
  id: string;
  iat: number;
  exp: number;
}

TokenPayload라는 인터페이스를 정의한다. 이는 앞으로 JWT에서 사용할 페이로드의 형식을 정의한다.

declare global {
  namespace Express {
    interface Request {
      userId?: string;
    }
  }
}

이 부분은 전역 타입 선언으로, Express의 Request 타입을 확장해 userId라는 요소를 추가한다.

 

더 상세히 보자면 declare global은 전역 타입을 선언하는 블록이다. 타입스크립트는 이 블록을 통해

 

기존에 존재하던 전역 스코프에 있는 타입을 확장하거나 새로운 타입을 추가하는 것을 허용한다.

 

다음으로 namespace Express 부분은 Express 라이브러리의 네임스페이스를 확장하겠다는 것을 나타낸다.

 

이를 통해 Express 라이브러리에서 정의한 타입에 새로운 타입을 추가할 수 있다.

 

마지막으로 interface Request는 Express 라이브러리에서 제공하는 Request 타입을 확장하겠다는 표시이다.

 

userId?: string은 Request 타입에 새로운 요소 userId를 추가한다.

 

여기서 물음표(?)는 해당 요소가 선택적이며, 존재하지 않을 수도 있다는 것을, 즉 필수 요소가 아니라는 것을 나타낸다.

const authMiddleware = (req: Request, res: Response, next: NextFunction) => {...}

이 부분은 authMiddleware라는 미들웨어 함수를 선언한다. 이 함수는 인증 토큰을 처리하는 로직을 포함한다.

const { authorization } = req.headers;

요청 헤더에서 authorization 값을 추출한다.

 if (!authorization) {
    return res.status(401).json({ error: 'No token provided' });
  }

authorization 값이 없다면, 즉 인증 토큰이 제공되지 않았다면, 401 상태 코드와 함께 에러 메시지를 응답으로 보낸다.

const token = authorization.replace('Bearer ', '').trim();

authorization 값에서 "Bearer" 문자열을 제거하고, 앞뒤 공백도 제거해 실제 토큰 값을 얻어 token에 할당한다.

try {
    const data = jwt.verify(token, process.env.JWT_SECRET_KEY || '');

    const { id } = data as TokenPayload;

    req.userId = id;

    return next();
  } catch {
    return res.status(401).json({ error: 'Invalid token' });
  }

try 블록 안에 jwt.verify를 사용해 토큰은 검증하는 코드를 넣는다.

 

만약 토큰이 유효하지 않다면 catch 블록으로 이동해 에러 메시지와 401 상태코드를 응답으로 보낸다.

 

const { id } = data as TokenPayload;에서는 data 객체를 TokenPayload 타입으로 단언(Assertion)하고 있다.

 

as를 사용한 타입 단언은 타입스크립트에서 컴파일러에게 특정 변수가 특정 타입임을 명시적으로 알려주는 방법이다.

 

이 경우 data 객체는 jwt.verify를 통해 반환된 값이며, 우리는 타입을 알고 있으나 타입스크립트 컴파일러가

 

그렇지 않을 경우가 있으므로 명시적으로 data의 타입을 TokenPayload로 보장하는 것이다.

 

그런 다음 const { id } = data;라는 구조 분해 할당을 통해 data에서 id필드를 추출하고 있다.

 

이렇게 가져온 id는 JWT에서 가져온 사용자의 아이디가 된다.

 

이 아이디는 req.userId = id;에서 Request 객체의 userId 프로퍼티에 저장된다.

 

이렇게 저장된 userId는 다른 미들웨어나 라우터 핸들러에서 사용될 수 있다.

 

계속해서 next()를 호출해서 요청-응답 사이클을 다음 미들웨어로 넘긴다.

 

내보내기는 생략한다.

 

구조 분해 할당(destructuring assignment)

 

구조 분해 할당은 배열이나 객체의 속성을 해체하여 그 값을 개별 변수에 할당하는 자바스크립트의 표현식이다.

 

이를 이용하면 복잡한 객체에서 원하는 데이터만 쉽게 추출할 수 있기 때문에 코드를 간결하게 만들어준다.

 

그 쉬운 예는 아래와 같다.

let [a, b] = [1, 2];
console.log(a); // 1
console.log(b); // 2

 

/src/controller

 

스프링부트의 MVC 구조에서와 마찬가지로, 가장 앞에서 요청을 받고, 적절한 모델을 호출해 처리한 뒤

 

응답 데이터를 생성하여 그 결과를 뷰로 전달하는 모듈이 모여있는 폴더이다.

 

userController.ts

 

import express from 'express';
import asyncHandler from 'express-async-handler';
import User, { UserDocument } from '../models/User';
import generateToken from '../utils/generateToken';

const registerUser = asyncHandler(async (req: express.Request, res: express.Response) => {
  const { email, password } = req.body;

  const userExists = await User.findOne({ email });

  if (userExists) {
    res.status(400);
    throw new Error('User already exists');
  }

  const user: UserDocument = await User.create({
    email,
    password
  });

  if (user) {
    res.status(201).json({
      _id: user._id,
      email: user.email,
      token: generateToken(user._id),
    });
  } else {
    res.status(400);
    throw new Error('Invalid user data');
  }
});

const authUser = asyncHandler(async (req: express.Request, res: express.Response) => {
  const { email, password } = req.body;

  const user: UserDocument | null = await User.findOne({ email });

  if (user && (await user.matchPassword(password))) {
    res.json({
      _id: user._id,
      email: user.email,
      token: generateToken(user._id),
    });
  } else {
    res.status(401);
    throw new Error('Invalid email or password');
  }
});

export {
  registerUser,
  authUser
};

이 모듈은 registerUser, authUser 두 개의 함수를 정의하고 있다.

 

두 함수는 모두 asyncHandler로 감싸져 있는데, 이는 비동기 함수 내에서 발생하는 에러를 처리하기 위한 것이다.

 

더 정확하게는 에러를 잡아내서 에러 처리 미들웨어로 전달하는 역할을 한다.

 

계속해서 두 함수는 모두 HTTP 요청을 처리하는 미들웨어로 사용되며, MongoDB와 상호 작용하며 유저 정보를 처리한다.

import express from 'express';
import asyncHandler from 'express-async-handler';
import User, { UserDocument } from '../models/User';
import generateToken from '../utils/generateToken';

먼저, 각 패키지에서 필요한 항목들을 가져온다.

const registerUser = asyncHandler(async (req: express.Request, res: express.Response) => {
  const { email, password } = req.body;

  const userExists = await User.findOne({ email });

  if (userExists) {
    res.status(400);
    throw new Error('User already exists');
  }

  const user: UserDocument = await User.create({
    email,
    password
  });

  if (user) {
    res.status(201).json({
      _id: user._id,
      email: user.email,
      token: generateToken(user._id),
    });
  } else {
    res.status(400);
    throw new Error('Invalid user data');
  }
});

이어서 첫 번째 비동기 함수, registerUser를 정의한다.

const { email, password } = req.body;

먼저 사용자의 이메일과 비밀번호를 요청 바디에서 추출한다.

const userExists = await User.findOne({ email });

  if (userExists) {
    res.status(400);
    throw new Error('User already exists');
  }

이어서 이메일이 데이터베이스에 이미 존재하는지 확인 후, 존재한다면 400 코드와 함께 에러 메시지를 던진다.

const user: UserDocument = await User.create({
    email,
    password
  });

존재하지 않는다면 이메일과 비밀번호를 이용해 새 사용자를 생성한다.

 

  • User.create
    mongoose에서 제공하는 메서드로, MongoDB에 새 도큐먼트를 생성하고 저장한다.
    더 정확하게는 주어진 객체를 사용해서 새로운 User 객체를 생성하고, 생성한 객체를 MongoDB에 저장한 뒤 해당 객체를 반환한다.
  • await: User.create 메서드가 비동기로 프로미스를 반환하기 때문에 사용된다. 비동기 함수를 동기 함수처럼 사용할 수 있게 한다.
  • user:UserDocument
    위에서 반환된 User 객체는 User 모델의 타입인 UserDocument 타입을 가진다.
    따라서 해당 객체를 user라는 상수에 UserDocument 타입으로 저장하고 있다.
if (user) {
    res.status(201).json({
      _id: user._id,
      email: user.email,
      token: generateToken(user._id),
    });
  } else {
    res.status(400);
    throw new Error('Invalid user data');
  }

사용자 생성에 성공하면 상태코드 201과 함께 생성된 사용자의 정보와 토큰을 응답 본문에 포함시킨다.

 

만약 생성에 실패하면 400 코드와 함께 에러메시지를 던진다.

const authUser = asyncHandler(async (req: express.Request, res: express.Response) => {
  const { email, password } = req.body;

  const user: UserDocument | null = await User.findOne({ email });

  if (user && (await user.matchPassword(password))) {
    res.json({
      _id: user._id,
      email: user.email,
      token: generateToken(user._id),
    });
  } else {
    res.status(401);
    throw new Error('Invalid email or password');
  }
});

다음으로 두 번째 비동기 함수, authUser를 정의한다.

const { email, password } = req.body;

마찬가지로 요청 바디에서 이메일과 비밀번호를 추출한다.

const user: UserDocument | null = await User.findOne({ email });

주어진 이메일을 이용해 DB에서 사용자를 찾는다.

 

User.findOne 메서드는 MongoDB에서 조건에 맞는 첫 번째 도큐먼트를 찾는다.

 

해당 메서드는 찾는 도큐먼트가 없다면 null을 반환하며, 때문에 user 상수는 UserDocument 또는 null로 저장된다.

  if (user && (await user.matchPassword(password))) {
    res.json({
      _id: user._id,
      email: user.email,
      token: generateToken(user._id),
    });
  } else {
    res.status(401);
    throw new Error('Invalid email or password');
  }

디비에 사용자가 존재하고, 비밀번호가 일치하는 경우 사용자 정보와 토큰을 담아 응답 본문에 담는다.

 

만약 사용자가 없다면 401 코드와 함께 에러메시지를 던진다.

export {
  registerUser,
  authUser
};

마지막으로 두 함수를 내보내 다른 모듈에서 임포트 해서 사용할 수 있도록 만든다.


여기까지 해서 기본적인 모듈 분석이 끝났다. 처음 보는 생태계라 다소 어려움이 있었지만,

 

일단 어느 정도 이해는 한 것 같다. 다음은 이 구조를 이용해 간단한 게시판을 스스로 만들어보는 것을 목표로 삼아야겠다.

 

아, 그와 별개로 이 프로젝트가 진행되는 순서에 따라 코드 분석은 (아마도) 계속될 것이다.

 

혼자서 Node.js로 개발하는 그날까지, 서비스 서비스!

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/06   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30
글 보관함