Next.js 에서 Request Body 를 검사하기

byron1st·2022년 11월 16일
0

Next.js 배우기

목록 보기
4/4

Next.js 는 백엔드 API 쪽에 대한 지원이 참 빈약하다. Logger 도 기본 제공이 되지 않고, 라우팅도 HTTP Method 에 따라 분기해줄 수 있는 app.post 과 같은 함수도 없다. 2022년에 if 또는 switch 문을 써서 req.method === 'POST'를 검사해 분기를 해주어야 하다니..

또 하나 빠져있는 주요한 기능이 Request Body 에 대한 검사다. 다행히 header의 Content-Type을 검사해서 request.body 객체를 만들어주는 건 해준다. 빠진 건, 이 Request body 의 타입을 검사하는 부분이다.

이전 글인 Go 언어로 HTTP API 서버에 필요한 것들에서 소개한 go-validator 같은 라이브러리가 JavaScript 에도 있다. ajv 라는 라이브러리인데, 주간 다운로드 수도 8,000만이 넘고, express 의 뒤를 잇는 차세대 웹 서버 프레임워크인 Fastify에서도 Request Body 를 파싱하는데 사용하고 있다.

ajv의 타입 검사는 크게 다음 단계로 진행된다.

  1. new Ajv() 를 통해 validator 객체를 만든다.
  2. validator 객체는 compile 이라는 함수를 갖고 있는데, 이 함수는 미리 자바스크립트 객체로 작성된 Schema 를 입력받아 해당 Schema 를 검사하는 validate 함수를 출력값으로 준다.

-- 여기까지는 전역변수로 미리 정의해둔다. 아래 부분은 API 가 호출 될 때마다 실행하면 된다 --

  1. 2단계에서 만든 validate 함수에 검사하고자 하는 객체를 넣는다. 출력값으로 true 또는 false가 나온다.
  2. false가 출력될 경우, 검사에 실패하고 뭔가 에러가 있다는 말이다. 해당 에러들은 validate.errors 를 통해 접근할 수 있다. validate 는 함수이지만, errors 라는 프로퍼티도 갖고 있다. 아주 자바스크립트다운(?) (거지같은) 구현이다. 이 errors 배열의 아이템은 객체인데, message 프로퍼티가 예쁘게 에러 메세지를 만들어준다.

코드를 보면 바로 이해가 된다.

import Ajv, { ValidateFunction, Schema } from 'ajv'

// 1. validator 객체를 만든다.
const validator = new Ajv()

// 2. Schema 는 미리 정의되어 있다.
const createSessionBodySchema: Schema = {
  type: 'object',
  properties: {
    idToken: { type: 'string' },
  },
  required: ['idToken'],
}

// 2. compile 을 하여, validate 함수를 만들어준다.
const validate = validator.compile(createSessionBodySchema)

async function createSession(req: NextApiRequest, res: NextApiResponse) {
  // 3. validate 함수에 검사하고자 하는 req.body 를 넣어준다.
  if (!validate(req.body)) {
    // 4. 반환값이 false 일 경우, errors 프로퍼티를 이용하여 적절한 에러 메세지를 출력해준다.
    res.status(400)
      throw new Error(validate.errors?.map((error) => error.message).join(','))
  }
}

여기서 Schema 정의는 JSON Schema(널리 쓰임), JSON Type Definition(표준임, RFC8927) 모두 지원한다. 링크를 참고하자.

validate 함수는 깔끔하게 true, false 만 반환하기 때문에 TypeScript 의 user defined type guard 와 조합하면 request.body 를 타이핑할 수도 있다. 아래와 같은 함수를 만들어보자.

function parseBody<T>(validate: ValidateFunction, body: unknown): body is T {
  return validate(body)
}

parseBody 함수는 validate 함수와 검사하고자 하는 객체 body를 받아서, 만약 validate 결과가 true 라면, body 객체가 generic으로 지정된 T 타입 임을 확인한다.

이 함수를 이용하여 위의 코드를 고치면 다음과 같다.

import Ajv, { ValidateFunction, Schema } from 'ajv'

const validator = new Ajv()

const createSessionBodySchema: Schema = {
  type: 'object',
  properties: {
    idToken: { type: 'string' },
  },
  required: ['idToken'],
}

interface CreateSessionBody {
  idToken: string
}

const validate = validator.compile(createSessionBodySchema)

async function createSession(req: NextApiRequest, res: NextApiResponse) {
  // 3. validate 함수에 검사하고자 하는 req.body 를 넣어준다.
  if (!parseBody<CreateSessionBody>(validateCreateSession, req.body)) {
    res.status(400)
      throw new Error(validate.errors?.map((error) => error.message).join(','))
  }
  
  // req.body 가 parseBody 함수에 의해 이미 type guard 되었으므로, idToken 은 별도로 타입 지정을 안해줘도 자동으로 string 으로 인식한다.
  const idToken = req.body.idToken
  
  ...
}

여기서 코드를 좀 더 분할하면, 아래와 같이 두 파일로 분리하여 리팩토링할 수 있겠다.

// services/ajv.ts

import Ajv, { ValidateFunction } from 'ajv'

export const validator = new Ajv()

export function parseBody<T>(validate: ValidateFunction, body: unknown): body is T {
  return validate(body)
}
// pages/api/users/[userId]/sessions.ts
import { NextApiRequest, NextApiResponse } from 'next'
import { withIronSessionApiRoute } from 'iron-session/next'
import { Schema } from 'ajv'
import { parseBody, validator } from 'services/ajv'
import { sessionMaxAge, sessionOptions } from 'services/session'

...

export default withIronSessionApiRoute(async (req: NextApiRequest, res: NextApiResponse) => {
  switch (req.method) {
    case 'POST':
      await createSession(req, res)
      return
    ...
    default:
      respond(res.status(404))
  }
}, sessionOptions)

const createSessionBodySchema: Schema = {
  type: 'object',
  properties: {
    idToken: { type: 'string' },
  },
  required: ['idToken'],
}

interface CreateSessionBody {
  idToken: string
}

const validateCreateSession = validator.compile(createSessionBodySchema)

async function createSession(req: NextApiRequest, res: NextApiResponse) {
  try {
    if (!parseBody<CreateSessionBody>(validateCreateSession, req.body)) {
      res.status(400)
      throw new Error(validateCreateSession.errors?.map((error) => error.message).join(','))
    }

    const idToken = req.body.idToken

    ...
  } catch (err) {
	...
  }
}
profile
Fullstack software engineer specialized for Blockchain

0개의 댓글