Express의 .json 구조 해부

Ryu·2022년 8월 24일
0

Express에서는 [요청에 관한 처리를 하는 함수]를 미들웨어라고 합니다. 미들웨어라고 할 때 상황에 따라 라우트 핸들러도 포함해서 미들웨어라고 하기도 하고 제외 하기도 합니다. 각각의 상황에 맞게 이해하면 됩니다.

미들웨어의 정확한 정의는 OS[운영체제]와 응용 프로그램 사이에서 동작하는 프로그램을 의미합니다.

함수 또한 작은 프로그램이라고 볼 수 있으니 Express에서 말하는 미들웨어 또한 미들웨어의 정의에 부합합니다.

우리는 Express에서 app.use(express.json()); 라는 코드를 추가해,

  • 요청 http 프로토콜의 [바디]에 들어있는 JSON [데이터]를 => [req 객체]의 [body 프로퍼티]에 설정하도록 했습니다.
  • express의 json 메소드는 미들웨어를 리턴하는 메소드이고, 미들웨어란 리퀘스트를 처리하는 기능을 하는 함수라고 했었는데요.

express의 json이라는 메소드는 어떤 미들웨어(함수)를 리턴하는 걸까요? 한번 코드를 파헤쳐봅시다.

그럼 한번 express의 맨처음 코드인 아래코드가 실행될때 참조되는 파일인 index.js 파일을 찾아가봅시다.

const express = require(‘express’);

index.js 파일을 로컬 스토리지의 express 디렉토리[폴더]에서도 찾을 수 있지만,
Express 는 오픈소스 프로젝트이기에 github에서 직접 들어가서 확인할 수 있습니다.

Express 깃 허브 페이지


https://github.com/expressjs/express

원본 코드를 보려고 들어갔드니 새로운 경로를 알려주시네...

위 사진에서 볼 수 있듯이.
index.js 파일은 lib디렉토리 안에 있는 express라는 파일에서 모듈을 가져오는군요.
그럼 해당 파일로 올라가봅시다. lib/express.js 파일을 열어서 그 상단 부분을 보면

express.js 파일에서는
createApplication라는 함수가 선언되어있고
createApplication 함수는 실행될 때 => app이라는 함수를 리턴한다는 것을 알 수 있습니다.
그러니까 결국 아래 코드에서 저희가 보았던 app 객체는 함수였던 것이죠.

const express = require('express');
const app = express();세요

그리고 express.js 파일에서 조금만 더 아래로 내려가보면 [70~80번째줄]

(바로 이전에 확인한 것처럼 exports는 createApplication의 함수를 가리킵니다)

이런 식으로 createApplication 함수에 json이라는 프로퍼티를 생성하고(만들고) 있습니다.

그렇기때문에 저희가 아래 코드를 처음에 입력한 순간부터 바로 json()을 쓸 수 있엇던 것 입니다.

const express = require(‘express’);
app.use(express.json());

그렇다면 지금 json 프로퍼티에 설정된 bodyParser.json이라는 건 무엇이고 어디에서 온 것 일까요?

bodyParser는 사실 express 패키지 내부에 없습니다.

express 패키지가 의존하고 있는 => 다른 패키지에 있는 객체인데요. 약간 어지럽죠..?

이따가 다시 한번 정리하겠습니다.
잠깐 express 패키지의 package.json 파일을 살펴보면,

express의 package.json파일을 보면 의존성관리 항목에 body-parser가 있다.

Express의 packge.json에 있는 의존성[dependencies] 패키지들 중에 body-parser라는 패키지가 있다는 걸 알 수 있습니다.
이 패키지를 봐야 json 함수의 정체를 온전히 파악할 수 있습니다. 타고들어가고 또 타고들어가서 또 타고들어가는...

그러고나서 npm 사이트에 들어가서 body-parser를 찾아보면 아래와 같은 화면이 나오는데.

위 내용을 정리하자면.
서버로 들어오는 요청을 라우트 핸들러에 도달하기 전에
리퀘스트의 바디를 파싱(parsing)해주는 기능을 가진 패키지라는 설명입니다.

라우트 핸들러란?

라우트 핸들러[Route Handler]란 특정 path에 지정된 메소드를 리퀘스트 처리해주는 함수를 의미합니다. 어떤 기능을 하냐면 => 지정된 경로에 맞춰서 리퀘스트를 처리해주거나 보통은 컨트롤러로 옮겨주는 역할을 수행하죠.

app.get('/api/members', async (req, res) => {
  const { team } = req.query;
  if (team) {
    const teamMembers = await Member.findAll({ where: { team } });
    res.send(teamMembers);
  } else {
    const members = await Member.findAll();
    res.send(members);
  }
});
 "여기서는 async로 시작되는 함수가 라우트 핸들러인 거죠."

그러니까 body-parser 패키지는 요청이 라우트 핸들러에 의해 처리되기 전에
먼저=> HTTP 응답의 바디에 관한 처리하는 기능을 갖고 있는 패키지입니다.
또한 body-parser에는 JSON 형식의 데이터를 파싱하는 기능도 있음을 알 수 있습니다.

자그럼 GitHub 링크를 클릭해서 body-parser 패키지에서 필요한걸 찾아옵시다.
마찬가지로 body-parser 패키지도 오픈소스입니다
이번에도 index.js 파일을 열고. 코드 중간을 보면 아래와 같은

일단 exports가 가리키는 객체가 설정되어있고

그 아래에 Object.defineProperty로 시작되는 코드는 exports 객체에 json이라는 프로퍼티를 설정하는 코드입니다.

지금 defineProperty 메소드의 세 번째 매개변수로 들어온 객체는 json이라는 프로퍼티 자체가 가질 속성들인데요.

여기서 get이라는 부분은 나중에 이 프로퍼티에 접근할 때 실행될 getter 함수를 의미합니다.

Recap!                    

express.json은
bodyParser.json을 의미하고
createParserGetter('json')의 리턴값을 가리킵니다! 

자 근데 createParserGetter의 리턴값은 무엇을 가르킬까요? ㅎㅎ...

createParserGetter 함수도 같은 파일 안에 들어있는데요.
createParserGetter 함수는 => get 함수를 리턴하네요.

이 get이라는 함수는 다시 loadParser 함수의 리턴 결과를 리턴합니다.
아래 코드를 보면서 loadParser함수도 한번 확인해봅시다.

loadParser 함수에는 switch case 문이 있습니다!

Swith case 조건문에서는 문자열 'json'을 발견하면
=> 또다른 파일에서 가져온 parser를 parsers 객체에 설정! 그후
=> parser 배열을 리턴합니다.

그러므로 parser 에는
=>lib/types/json.js 파일에서 가져온
=>parser들이 들어간 배열이 됩니다.

parser만으로 이루어진 parser 타입같은 배열이랄까요?

슬슬한계가 오고 있겠지만 정말 곧 끝입니다!
거의 다 왔어요!

아래 코드에 보이는 ./lib/types/json 파일로 가보겠습니다.

자, 이 파일은 json이라는 함수를 리턴하고 있습니다. 그리고 json 함수는 결국 jsonParser라는 함수를 리턴합니다.

body-parser의 json파일 내부 json함수에 대한 코드를 보시면

json 함수가 => jsonParser 함수를 리턴하는걸 볼 수 있습니다!

Velog에서는 접은글 기능을 사용할 수 없네요..
코드가 너무 길기 때문에 건너뛰시려면 스크롤을 많이 내리셔야합니다.

function json (options) {
  var opts = options || {}

  var limit = typeof opts.limit !== 'number'
    ? bytes.parse(opts.limit || '100kb')
    : opts.limit
  var inflate = opts.inflate !== false
  var reviver = opts.reviver
  var strict = opts.strict !== false
  var type = opts.type || 'application/json'
  var verify = opts.verify || false

  if (verify !== false && typeof verify !== 'function') {
    throw new TypeError('option verify must be function')
  }

  // create the appropriate type checking function
  var shouldParse = typeof type !== 'function'
    ? typeChecker(type)
    : type

  function parse (body) {
    if (body.length === 0) {
      // special-case empty json body, as it's a common client-side mistake
      // TODO: maybe make this configurable or part of "strict" option
      return {}
    }

    if (strict) {
      var first = firstchar(body)

      if (first !== '{' && first !== '[') {
        debug('strict violation')
        throw createStrictSyntaxError(body, first)
      }
    }

    try {
      debug('parse json')
      return JSON.parse(body, reviver)
    } catch (e) {
      throw normalizeJsonSyntaxError(e, {
        message: e.message,
        stack: e.stack
      })
    }
  }

  return function jsonParser (req, res, next) {
    if (req._body) {
      debug('body already parsed')
      next()
      return
    }

    req.body = req.body || {}

    // skip requests without bodies
    if (!typeis.hasBody(req)) {
      debug('skip empty body')
      next()
      return
    }

    debug('content-type %j', req.headers['content-type'])

    // determine if request should be parsed
    if (!shouldParse(req)) {
      debug('skip parsing')
      next()
      return
    }

    // assert charset per RFC 7159 sec 8.1
    var charset = getCharset(req) || 'utf-8'
    if (charset.slice(0, 4) !== 'utf-') {
      debug('invalid charset')
      next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', {
        charset: charset,
        type: 'charset.unsupported'
      }))
      return
    }

    // read
    read(req, res, next, parse, debug, {
      encoding: charset,
      inflate: inflate,
      limit: limit,
      verify: verify
    })
  }
}

그럼 한번 더 정리하자면.

Express 프레임워크에서 우리가 쓰는 express.json()
이것은 지금 보이는 jsonParser라고 하는 함수를 리턴하는 부분이었던 겁니다.

app.use(express.json());

좀 더 구체적으로 설명하자면.

맨 처음 우리가 추상적으로 썻던 위의 코드는
app.use(jsonParser);
사실 jsonParser를 json()으로 표현한 것이고.
jsonParser 함수의 파라미터는 req, res, next 라는 3개의 파라미터가 있고.
마지막 next 파라미터로 넘어오는 함수를 마지막에 호출함으로서.

function jsonParser (req, res, next) {
...
...
  next();
}

HTTP 요청 / 응답 을 다음 미들웨어나 라우트 핸들러로 넘기는 것. 이라고 이해하면 되겠습니다.

jsonParse에 대한 더 구체적인 흐름은 위에 첨부한 json function 코드를 참고하시고.

parse와 통신 과정에서의 정확한 원리는  IETF 국제 인터넷 통신 표준 협회의 HTTP 통신에 관한 문서에서 parse와 패킷에 대한 부분을 확인해주세요.
[저 협회에서 HTTP, HTTPS, HTTP/3 , WEB3.0 등의 iso 프로토콜 표준을 만든답니다.]
https://www.rfc-editor.org/rfc/rfc9110.html

긴글 읽으시느라 정말... 수고하셨습니다!

profile
호기심이 많은 사람입니다.

0개의 댓글