TIL [HTTP 트랜잭션 해부 / Mini Node Server 스프린트 코드 분석]

서버 생성

모든 node웹 서버 애플리케이션은 createServer를 써서 웹 서버 객체를 만들어야 한다. HTTP server에 대한 설명(https://nodejs.org/api/http.html#http_http_createserver_requestlistener)을 보니 HTTP server를 사용하기 위해서는 require('http')를 써야 한다고 나와있다.

위 코드는 실제 스프린트에서 쓰인 코드이다. 맨 윗 줄을 보니 실제로 require('http') 코드가 쓰인 것을 확인할 수 있다.

HTTP 트랜잭션 해부 문서에서 캡쳐해온 코드이다. 이 서버로 오는 HTTP 요청마다 createServer에 전달된 함수가 한 번씩 호출된다.

const server = http.createServer((request, response) => {
 // 여기서 작업이 진행됩니다!
});

createServer가 반환한 server 객체는 EventEmitter이고 여기서는 server 객체를 생성하고 리스너를 추가하는 축약 문법을 사용한 것이다. 축약을 하지 않으면 원래 아래와 같은 모양이다.

여기서 EventEmitter는 뭘까? EventEmitter에 관련된 공식 문서(https://nodejs.org/api/events.html#events_class_eventemitter)에 의하면 모든 이벤트를 발생시키는 객체(objects)는 EventEmitter의 인스턴스이다. 즉, EventEmitter는 인스턴스를 생성하는 클래스라는 뜻이다. 이 객체들은 한 개 이상의 함수를 특정 event에 붙여서 실행될 수 있게 허락하는 .on() 함수를 가지고 있다. 이 객체들이 이벤트를 발생(emit)시키면 그 특정 event에 붙어있는 한 개 이상의 함수들을 전부 비동기적으로 호출시킨다. 호출된 함수들로 인해 발생한 어떠한 리턴 값은 다 무시되고 버려진다.

다음에 나올 예시는 EventEmitter 클래스를 통해 인스턴스를 생성하고 그 인스턴스들을 통해 .on() 함수를 실행할 것이다.

const EventEmitter = require('events'); // EventEmitter를 쓰기 위해 먼저 설정하는 부분

class MyEmitter extends EventEmitter {} // EventEmitter 클래스를 상속받는 MyEmitter라는 자식 클래스를 생성

const myEmitter = new MyEmitter(); // MyEmitter 클래스를 통해 인스턴스 생성
myEmitter.on('event', () => { // 인스턴스에 on 함수를 적용.
            // event는 이벤트의 이름이고 뒤에 붙는 함수는 이 이벤트에 붙는 함수이다.
  console.log('an event occurred!');
});
myEmitter.emit('event'); // event라는 이벤트에 붙은 함수를 실행준다.

위에서 설명했듯이 createServer가 반환한 server 객체는 EventEmitter이기 때문에 server.on이 가능하다!

다시 여기로 돌아왔다. HTTP 요청이 들어오면 node가 트랜잭션을 다루려고 request와 response 객체를 전달하며 요청 핸들러 함수를 호출한다.


메서드, URL, 헤더

우리는 request를 가지고 작업을 할 것이기 때문에 request 객체를 비구조화 할당을 통해 변수에 선언해주고 시작하는게 좋다.

밑에 코드는 내가 실제로 스프린트에 썼던 코드이다. 공식 문서처럼 비구조화 할당을 했기 때문에 앞으로 method나 url은 request.method, request.url을 의미하게 된다. request 객체는 IncomingMessage의 인스턴스라고 하는데 공식 문서(https://nodejs.org/api/http.html#http_class_http_incomingmessage)를 찾아보니, IncomingMessage 객체는 http.Server나 http.ClientRequest에 의해 만들어 졌다고 한다.

server라는 객체는 EventEmitter이므로 .on()함수를 쓸 수 있고, 'request'라는 이벤트에 (request, response) => {} 함수가 붙은 것을 알 수 있다. 또한 request와 response 객체는 'request' 이벤트에 붙은 함수의 파라미터로 들어갔다.

다시 본론으로 돌아와서 변수 method는 request.method인데 이때 method는 항상 일반적인 HTTP 메서드(GET, POST< PUT, DELETE 등)가 될 것이다. 변수 url은 전체 URL에서 서버, 프로토콜, 포트를 제외한 것으로, 세 번째 슬래시 이후의 나머지 전부라고 볼 수 있다.

실제로 스프린트에서 url을 콘솔 창에 찍어보니

/upper, /lower가 나오는 것을 확인할 수 있다.

App.js를 보니, 내가 누르는 버튼(toUpperCase나 toLowerCase)에 따라 URL이 'http://localhost:5000/upper' 나 'http://localhost:5000/lower' 로 바뀌고 url은 서버, 프로토콜, 포트를 제외한 /upper, /lower인 것을 확인할 수 있다.


요청 바디

본문으로 들어가기 전에 Preflight 요청 예시에 대해 알아보자. Preflight는 요청 메소드가 서버 데이터에 부수 효과를 일으킬 수 있는 HTTP 요청 메소드(GET을 제외한 HTTP 메소드) 일 경우, 본 요청이 바로 오는게 아니라 OPTIONS 메소드를 가진 Preflight 요청을 먼저 보낸다. Preflight 요청은 이렇게 생겼다.

Preflight 요청의 헤더에는 Access-Control-Request-Method, Access-Control-Request-Headers 및 Origin이 들어간다. 위 사진은 브라우저가 서버에게 "본 요청의 메소드는 POST이고, HTTP 헤더로는 X-PINGOTHER, Content-Type, 출처는 https://foo.example인데 가능한지 미리 확인좀 해줘." 라고 질문을 하는 것이다. 이 preflight 요청에 대한 응답으로 서버는 다음과 같은 메세지를 보낸다.

위의 preflight에 대한 응답을 해석하면 "요청 가능한 origin은 http://foo.example, 너가 쓸 수 있는 메소드 종류는 POST, GET, OPTIONS이고...." 라고 할 수 있다. 이렇게 preflight 요청과 응답이 끝나면 본 요청이 브라우저에서 서버로 전달된다.

이번 스프린트를 진행하면서 제일 힘들었던 부분이다. 일단 response 객체를 보면 HTTPserver에 의해 내부적으로 생성이 된다. 그리고 'request' 이벤트에 두 번째 파라미터로 들어가게 된다.

response.writeHead를 공식 문서(https://nodejs.org/api/http.html#http_response_writehead_statuscode_statusmessage_headers)에서 확인하면 response.writeHead(statusCode[, statusMessage][, headers]) 이렇게 쓸 수 있다고 한다.

여기서 statusCode에는 200이, headers로는 defaultCorsHeader가 들어간다. 여기서 defaultCorsHeader는 스프린트 제일 밑에 이미 만들어진 연관 배열이다.

그럼 writeHead 메소드는 무슨 역할을 할까? writeHead 메소드는 클라이언트(브라우저)에 보낼 response의 header부분을 담당한다고 보면 된다. 즉, defaultCorsHeader 부분이 서버(response를 보낸쪽)에서 클라이언트(request를 보낸쪽)에게 보내는 메세지이다. "나는 'Access-Control-Allow-Origin' 부분을 '*'(모든 origin 허용)로 가지고 있고 'Access-Control-Allow-Methods' 부분을 'GET, POST, PUT, DELETE, OPTIONS' 로 갖고 있고... 따라서 이것에 맞춰서 보내" 라고 생각하면 될 것 같다.

이 코드를 분석해보면 다음과 같다.

1) 'Access-Control-Allow-Origin': '*'

스프린트 초반에 이런 에러가 떴었다. 바로 CORS 에러가 뜬 것이다. 브라우저와 서버가 다른 origin(출처)를 가지고 있다면 크롬 같은 브라우저에서는 same origin policy(같은 출처 정책)로 인해 브라우저가 서버의 리소스에 접근하는 것을 막아버린다.

MDN에서 찾은 Access-Control-Allow-Origin의 의미는 이것이다.

Access-Control-Allow-Origin 응답 헤더는 이 응답이 주어진 origin으로부터의 요청 코드와 공유될 수 있는지를 나타냅니다.

처음 브라우저가 서버에 요청을 보낼때 요청 메세지의 start line은 예시로 'POST/ HTTP/1.1' 이렇게 생겼는데 요청
메소드가 서버 데이터에 부수 효과를 일으킬 수 있는 HTTP 요청 메소드(GET을 제외한 HTTP 메소드) 일 경우, 본 요청이 바로 오는게 아니라 OPTIONS 메소드를 가진 preflight(사전 전달)를 먼저 서버로 보내게 된다. 그러면 서버에서는 preflight에 대한 대답을 해준다. 그 대답이 바로 response header(응답 헤더)이고 그 response header가 주어진 origin에게 Access-Control-Allow-Origin을 통해 요청 코드와 공유될 수 있는지 나타낸다. 'Access-Control-Allow-Origin': '*'의 의미는 어떠한 origin과도 공유를 허락한다는 의미이다. 따라서 브라우저와 서버가 다른 origin을 갖고 있어도 CORS 에러를 띄우지 않겠다는 의미이다.

'*' 외에도 Access-Control-Allow-Origin에 가능한 것은

특정 origin만 허락하거나 아예 허락을 안 하는 경우가 있다.

2) 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS'

위에서 언급한 대로, 처음 브라우저가 서버에 요청을 보낼때 요청 메소드가 서버 데이터에 부수 효과를 일으킬 수 있는 HTTP 요청 메소드(GET을 제외한 HTTP 메소드) 일 경우, OPTIONS 메소드를 가진 preflight(사전 전달)를 먼저 서버로 보낸다고 했다.

preflight를 받은 서버는 이에 대한 응답을 보내게 되는데 마찬가지로 response header(응답 헤더)에 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS' 같이 명시하면 서버는 요청 메소드로 'GET, POST, PUT, DELETE, OPTIONS' 만 허락하겠다는 뜻이다.

3) 'Access-Control-Allow-Headers': 'Content-Type, Accept'

MDN에서 찾은 Access-Control-Allow-Headers의 의미는 이것이다.

Access-Control-Allow-Headers 는 Access-Control-Request-Headers를 포함하는 preflight request의 응답에 사용되는 헤더로, 실제 요청때 사용할 수 있는 HTTP 헤더의 목록을 나열합니다.

즉, 요청 메소드가 POST, PUT 같은 서버에 부수 효과를 일으킬 수 있는 요청은 preflight를 먼저 서버로 보내게 되고, 이에 대한 대답으로 실제 요청때 어떤 헤더를 쓸 수 있는지 그 목록을 나열해서 보내주게 된다.

4) 'Access-Control-Max-Age': 10

MDN에서 찾은 Access-Control-Max-Age의 의미는 이것이다.

마지막으로Access-Control-Max-Age는 다른 preflight request를 보내지 않고, preflight request에 대한 응답을 캐시할 수 있는 시간(초)을 제공합니다.

브라우저에서 보낸 preflight에 대한 응답을 캐시할 수 있는 시간(초)를 제공한다. 위의 코드는 10초를 의미한다. 각 브라우저의 최대 캐싱 시간은 Access-Control-Max-Age가 클수록 늘어난다.

본문으로 돌아와서 response.writeHead를 통해 preflight 요청에 대한 응답을 하고 reponse.end()를 호출하여 콘텐츠 출력하며, preflight 요청에 대한 응답 처리는 종료된다. reponse.end()에 아무것도 쓰여있지 않으면 아무런 콘텐츠가 없는 것이다. "writeHead", "write", "end"의 3개가 있으면, 클라이언트에 반환 내용은 모두 쓸 수 있다.

아까 비구조화 할당으로 인해 method와 url이 각각 request.method와 request.url이라고 했다. 만약 요청이 왔는데 method가 'POST'이고 url이 '/upper'나 '/lower'일 경우 어떻게 해야 할지 정하는 코드이다.

일단 url이 '/upper'일 경우부터 살펴보겠다.

data라는 변수를 빈 문자열로 지정해줬고 request.on을 이용했다. 공식 문서에서 가져온 .on() 함수는 이렇다.

먼저 eventName으로는 문자열이 들어가고 이것이 이벤트의 이름이 된다. 위의 예시에서는 'data'라는 문자열이 들어갔기 때문에 'data'라는 이벤트가 생성이 된 것이다. 그 다음 파라미터는 콜백 함수이고 여기서는 listener라고 하는데 위의 예시에서는

(chunk) => {
  data = data + chunk;
}

이러한 콜백 함수가 listener로 설정이 된 것을 볼 수 있다. 콜백 함수의 파라미터인 chunk가 뭔지 궁금하여 콘솔 창에 찍어보니

chunk는 Buffer라고 한다. 여기서 Buffer란 데이터를 한 곳에서 다른 곳으로 전송하는 동안 일시적으로 그 데이터를 보관하는 메모리 영역을 의미한다.

우리가 유튜브를 볼 때 빨간색 부분은 지금 내가 시청하는 부분이고 밝은 회색은 내가 서버로 부터 동영상을 내려받은 부분이라고 할 수 있다. 우리가 동영상을 볼 때 보는 속도와 데이터를 서버에서 받는 속도에 위 사진 처럼 차이가 있는데 이 차이가 나는 부분을 임시 저장 공간에 두어 유저가 기다리지 않고 바로 볼 수 있도록 하는게 바로 버퍼이다. 만약 빨간색 부분이 밝은 회색을 더 빨리 따라잡는다면 우리가 보던 영상은 멈추게 되는데 이것을 바로 버퍼링이라고 한다.

다시 본론으로 돌아오면, chunk는 내가 브라우저에서 받은 JSON 형식의 파일을 임시로 저장하는 보관소 인 셈이다. 해당 스프린트에서는 우리가 입력 창에 뭔가를 적고 toUpperCase나 toLowerCase 버튼을 눌러, JSON 파일을 서버로 전송하게 되는데 이때 입력 창에 적은 글이 JSON 파일화되서 서버로 전송되기 때문에 JSON.parse(chunck)를 하면 그 내용을 볼 수 있다.

나는 입력 창에 hello를 적었고 toUpperCase를 눌러보았다.

그리고 콘솔 창에 JSON.parse(chunck)를 찍어보니

예상대로 내가 입력 창에 적은 hello가 임시 저장된 것을 확인할 수 있었다.

마지막으로 'end' 이벤트를 이어 붙인 다음 변수 data에 저장된 문자열을 대문자로 바꿔줬고, response.writeHead(201, defaultCorsHeader)를 통해 서버가 다시 브라우저에게 요청에 대한 응답을 보내줬다. response.end(data)에서 data는 App.js 파일로 다시 돌아간다.

그리고 data는 .then(res ...)에서 이 res가 된다.


느낀점

이번 스프린트에서 브라우저와 서버가 어떻게 연결되어 통신을 하는지에 대해 알아봤고, 서버속에서 브라우저에서 보낸 JSON 형식의 데이터를 원하는 모양으로 가공한 뒤 다시 브라우저로 보내는 것 까지 배워보았다. 모르는 개념 A에 대해 구글링을 하니 거기서 모르는 개념 B, C가 발생했고 결국 꼬리를 물듯이 B, C를 알아야지만 A를 알 수 있었기에 더욱 힘들고 시간이 오래 걸렸던 것 같다. 아직도 모르는 개념들이 수두룩하고 이번 스프린트를 진행하느라 찾아본 공식 문서가 수십개는 되는것 같다..

스프린트 한 개를 끝낼때마다 엄청 많이 공부한 것 같은데 다음 스프린트를 진행하면 처음 배워야 할 것이 또 엄청나게 많기 때문에 공부를 뒤로 미룬다는 것은 상상도 할 수 없다. 그리고 항상 대부분의 개념들이 복잡한 세계관 처럼 서로 연결의 연결이 되어있기 때문에 하나를 놓치게 되면 다음에 배울 것들도 매우 힘들어 진다.

오늘이 금요일이고 내일은 주말이라 오늘 마음먹고 좀 빡세게 했는데 나름 결과가 좋은 것 같아서 기분은 좋다. 하지만 내일도 Toy 다시 풀기, 스프린트 다시 한 번 풀어보기, 블로깅 등등 할 일이 산더미이기 때문에 눈물이 난다ㅠㅠ 역시 프로그래밍 공부에는 끝이 없고, 배운 것보다 앞으로 배워야할게 더 많다는 것을 피부로 느낀 날이였다.

자료 출처:

좋은 웹페이지 즐겨찾기