Node.js | User(회원가입) API 구현

Hyeonju L.·2021년 1월 26일
5

Node.js

목록 보기
4/4

Nodejs session - blog 데이터 모델링 & 회원가입 API 구현하기
(타입스크립트 기반)

1. 데이터모델링

users와 articles 테이블 두개에 대한 모델링 진행.
nodejs에 익숙해지기 위한 연습이니 필수정보만 이용해 데이터모델링을 했다.

2. 회원가입 API 구현 (feat. express)

API를 구현하기 전에 한번 더 기억해야할 것.
Node.js에서 각각의 layer는 바로 하위에 있는 layer에만 의존한다.

Routes

index.ts

// 'express'에서 제공해주는 router를 이용해 route(길)을 연결
import { Router } from 'express'	// 'express'에서 Router라는 함수를 가져옴
import UserRouter from './UserRouter' // UserRouter 파일에서 UserRouter를 가져옴

const router = Router()

/* 'router의 use라는 메소드를 사용해서 /users라는 자원에 대해 UserRouter를 연결해주겠다'라고 선언. 
즉, endpoint가 '/users'로 요청이 들어오면 UserRouter로 보내짐 */
router.use('/users', UserRouter)

export default router

UserRouter.ts

import { Router } from 'express'
/* '../controllers/index.ts'의 UserController 객체에서 바로 구조분해 할당한 것
(객체에서 필요한 key value를 꺼내오듯이 모듈도 객체에서 꺼내올 수 있다) */
import { UserController } from '../controllers'  


const router = Router()

// '/signup', '/login'이면 UserController에 signUp/logIn 함수로 엔드포인트에 맵핑 
router.use('/signup', UserController.signUp) 
router.use('/login', UserController.logIn)

export default router

Controllers

index.ts

Controllers의 index.ts의 역할은 python의 init.py와 같은 역할로, 모듈을 패키지화 해서 내보낸다. js에서 모듈을 내보낼 때에는 항상 객체이다.

import UserController from './UserController'

// 아래 export를 통해 객체를 mapping하는데, UserController라는 것이 객체로 나가게 된다.
export {
  UserController	// key와 value값이 동일하면 이름만 쓰면 됨(UserController: UserController)
}

UserController.ts

꺼내온 객체에 함수를 직접 구현. try-catch로 구현하는 것이 controller의 일반적인 로직

import { Request, Response, NextFunction } from 'express'    // 'express'라는 모듈에서 타입을 꺼내옴
import { errorGenerator } from '../errors'	// 에러핸들링을 위해 ../errors/index.ts에서 errorGenerator 객체를 가져옴
import bcrypt from 'bcryptjs' 
import { UserService } from '../Services'


/* express를 사용해서 endpoint를 구현한다는 것은 함수를 구현한다는 의미. 
요청을 처리하는 함수를 구현할 때, 인자로 들어오는 것이 약속되어 있는데 바로 req, res 객체이다.*/
const signUp = async (req: Request, res: Response, next : NextFunction) => {
  try{	// try: error를 내는 부분
    const { email, password } = req.body  // request의 body에서 email과 password를 꺼내 구조분해 할당
    if (!email || !password) errorGenerator({ message: 'invalid input', statusCode: 400})	
    
    // DB와 소통을 해야하기 때문에 service의 로직 활용
    const foundUser = await UserService.findUser({ email }) 
    if (foundUser) errorGenerator({ statusCode: 409 })	// email이 중복인 경우 statusCode: 409 보냄. errorGenerator.ts의 message와 맵핑
    
    const hashedPassword = await bcrypt.hash(password, 10)
    
    const createdUser = await UserService.createUser({ email, password: hashedPassword})
    
    res.status(201).json({ message: 'created', createdUserEmail: createdUser.email })
    
  } catch(err) {   // try문에서 던진 error를 catch해서 next()로  내보냄
    next(err)
  }
}

// UserController에서 구현한 함수가 객체에 맵핑됨
export {
  signUp,
  logIn
}

에러 핸들링

컴퓨터는 위에서부터 순차적으로 코드를 읽어나간다. 읽다가 routes로 빠지면 routes/index.ts로 가면 request의 endpoint를 보고 UserRouter를 거쳐 UserController의 signUp 함수로 요청이 간다.

app.ts

import express, { Express } from 'express'
import { generalErrorHandler } from './errors'
import morgan from 'morgan'
import routes from './routes'

const logger = morgan('dev')

const app: Express = express()

app.use(express.json())
app.use(logger)
app.use(routes) // UserController의 signUp함수의 try문에서 email 또는 password가 없는 경우 error를 발생시키고, catch에서 next(err)를 통해 app.ts로 돌아와 다음 미들웨어를 실행시킨다.

app.use(generalErrorHandler) // 그럼 app.ts에서 다음 미들웨어인 generalErrorHandler로 넘어감. 즉 routes에서 발생한 모든 에러는 generalErrorHandler로 넘어가서 처리하게끔 코드 구현 

export default app

generalErrorHandler.ts

에러를 한 번에 받을 수 있도록 미들웨어를 만들어줌.

import { ErrorRequestHandler, Request, Response, NextFunction } from 'express'
import { ErrorWithStatusCode } from './errorGenerator'

// error를 handling하는 미들웨어는 err, req, res, next의 4개 인자를 받는다.
const generalErrorHandler: ErrorRequestHandler = (err: ErrorWithStatusCode, req: Request, res: Response, next: NextFunction) => {
  const { message, statusCode } = err // 에러 발생 시 err 객체를 만들때 받은 message와 statusCode를 구조분해 할당으로 가져옴 
  console.error(err)  // 에러가 난 지점 확인
  res.status(statusCode || 500).json({ message })  // statusCode와 message를 mapping해서 응답, response에 json method를 확인하여 보내줌  
}

export default generalErrorHandler

errorGenerator.ts

모듈화가 된 errorGenerator라는 함수를 이용해 에러 발생시키기.

const DEFAULT_HTTP_STATUS_MESSAGES = {
  400: 'Bad Requests',
  401: 'Unauthorized',
  403: 'Foribdden',
  404: 'Not Found',
  409: 'duplicate',
  500: 'Internal Server Error',
  503: 'Temporary Unavailable',
}

// 모듈화를 탄탄하게 하기 위해 타입스크립트의 interface 기능 이용 > 원래 있던 Error 객체에 statusCode라는 key값 추가
export interface ErrorWithStatusCode extends Error {
  statusCode?: number
}

// 1. 에러를 만들고 2. statusCode 저장 3. error throw
const errorGenerator = ({ message = '', statusCode = 500 }: { message?: string, statusCode: number}): void => {
  /* 우리가 만드는 에러는 statusCode가 함께 있는 에러이다(err: ErrorWithStatusCode)
  인자로 들어오는 message와 statusCode mapping */
  const err: ErrorWithStatusCode = new Error(message || DEFAULT_HTTP_STATUS_MESSAGES[statusCode])
  err.statusCode = statusCode	
  throw err
}

export default errorGenerator

ref. error handler를 이용하지 않고 에러처리
UserController에서 generalErrorHandler를 이용하지 않고 에러처리가 가능하다. 그러나 코드의 반복을 줄이고 같은 기능을 하는 코드는 모듈화를 시키는 것이 좋은 코드이기 때문에 모듈화를 하는 것이 좋다.

const signUp = (req: Request, res: Response, next: NextFunction) => {
  try {
    const { email, password } = req.body
    if (!email || !password) {
      const err = new Error('invalid input')
      err.statusCode = 400
      throw err
    } catch(err) {
      res.status(400).json({ message: err.message })
    }    
  }}

Services

DB와 소통하기 위한 로직 구현을 위해 ORM(prisma) 사용

index.ts

import UserService from './UserService'

export {
  UserService	// 객체 내보내기
}

UserService.ts

UserService 또한 함수를 여러개 가지고 있음 - 해당 함수 기능 구현을 위한 로직을 짜야한다. (Usercontroller에서 참조하고 있는 함수들: findUser, createUser)

import prisma from '../prisma'

/* 서비스 로직 구현 시 인자로 받는 것에 대한 interface를 미리 만들어주는 것이 좋다.
interface 만들어줌 - 함수로 들어오는 인자를 미리 선언하여 함수에 인자가 어떤 것이고 어떤 응답을 하는지 미리 알 수 있다.(typescript를 사용하는 이유) */
export interface userCreateInput {
  email: string,
  password: string
}

const createUser = (data: userCreateInput) {
  return prisma.users.create({ data })
}
/* 원래는 아래와 같이 작성되어야 하는데, 미리 interface로 인자를 미리 선언해주었기 때문에 아래와 같이 풀어쓰지 않아도 됨
  return prisma.users.create({ data: {
    email: data.email,
    password: data.password
  }}) */

export interface userUniqueSearchInput {
  id? : number,	// 인자로 들어올 객체에 대해 미리 선언
  email? : string
}

// ts의 interface 기능을 이용하여 인자로 들어올 객체에 대해 상단에서 미리 선언을 하고 함수에 mapping 시켜줌
const findUser = (data: userUniqueSearchInput) => {	
  const [uniqueKey] = Object.keys(data)	// Object.keys(): 배열로 객체의 key값을 내보냄, 구조분해할당 문법: uniqueKey로 변수명에 mapping해줌 
  return prisma.users.findUnique({ where: { [uniqueKey]: data[uniqueKey]} })
  // 사용하려는 자원에 대해 .을 찍고 findUnique 사용(유니크한 row 하나를 찾아내겠다는 method)
  // uniqueKey에 id가 들어오면 data[id]를 찾고, email이 들어오면 data[email]을 찾도록 함 (함수 모듈화 - interface를 이용해 명확한 함수의 역할 정의 가능)
  
}

export default {
  createUser,
  findUser
}
profile
What you think, you become. What you feel, you attract. What you imagine, you create.

1개의 댓글

comment-user-thumbnail
2021년 1월 28일

현주님 힘들어하시던게 엊그제인데 노드로 회원가입을 끝장내버리시다니 ..... 👏
사랑해요

답글 달기