Node Server

be kid·2022년 1월 28일
0

BEB - 웹 개발 과정

목록 보기
20/28

오늘의 1차 목표는 클라이언트가 보내온 POST 요청을 받아 원하는 응답을 보내주는 것이었다.
자세히는, 알파벳 문자열을 입력하고 toUpperCase 버튼을 누르면 대문자로 변환되어 돌아오고 toLowerCase를 누르면 소문자로 변환되어 돌아오도록 해야했다.

2차 목표는 Express를 사용해 서버 코드를 리팩토링하는 것이었다.

결과적으로는 모두 완수하긴 했지만 그 과정이 굉장히 재미있었다. 아주 다양한 에러와 문제 상황에 부딪히며 페어와 해결해나가는 것이 공부가 많이 된 것 같다.


브라우저에는 서버에 요청을 보내기 위해 fetch같은 HTTP 요청을 보내는 도구가 기본적으로 내장되어 있다. 그리고 서버는 클라이언트(브라우저)의 HTTP 요청에 알맞은 응답을 보낼 수 있도록 코드를 작성해야 한다. node.js는 HTTP 요청을 보내거나, 응답을 받을 수 있는 도구를 제공한다. HTTP 요청을 처리하고 응답을 보내주는 프로그램을 웹 서버 (Web Server)라고 부른다.

nodemon 사용하기

npm i -g nodemon 을 통해 nodemon을 설치한 뒤 package.json의 scripts에
"start": "nodemon server.js" 처럼 실행할 서버 앱을 적어준다.
npm run start 를 통해 서버를 실행하면, 서버 파일을 수정할 때마다 서버를 재시작해줘야 하는 번거로운 작업을 nodemon이 대신 수행해준다.

Node app 디버깅

"node --inspect server.js" 를 scripts등을 통해 실행한 뒤에 브라우저에서 개발자 옵션을 보면 노드 아이템이 추가가 된다. 이것을 확인해보면 console.log로 찍은 값을 트리 형태로 편하게 볼 수 있다.
"node --inspect-brk server.js" 로 하면 브레이크를 통해 디버깅을 할 수 있다.
node 대신 nodemon을 써도 똑같이 작동한다.

node.js의 HTTP 모듈을 이용해 웹 서버 만들기

const http = require("http");
const PORT = 4999;
const ip = "localhost";

// 클라이언트로부터 어떤 요청(request)이 있을 때마다 createServer에 전달된 함수가 한 번씩 호출됨
// request에는 요청을 할 때 보내온 정보들이 담겨있다.
const server = http.createServer((request, response) => {
	console.log(`http request method is ${request.method}, url is ${request.url}`);

	response.writeHead(200, defaultCorsHeader); // 상태코드와 CORS Header
	response.end("hello server"); // 응답을 위한 end
});

// 서버가 돌아가기 시작한다.
server.listen(PORT, ip, () => {
	console.log(`http server listen on ${ip}:${PORT}`);
}

const defaultCorsHeader = {
	"Access-Control-Allow-Origin": "*", // 모든 도메인을 허용
	"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", // 해당 메소드 허용
	"Access-Control-Allow-Headers": "Content-Type, Accept", // 헤더에는 Content-Type, Accept만 허용
	"Access-Control-Max-Age": 10 // pre flight request는 10초까지 허용
}

위 코드가 가장 기본적으로 클라이언트가 뭔가 요청했을 때 최소한의 반응을 보일 수 있는 서버라고 할 수 있겠다.

이제부터 해야할 일은 어떤 문자열을 적고 '대문자로 바꾸기' 또는 '소문자로 바꾸기'를 누르면 그에 맞은 응답을 보내주어야 한다.

https://nodejs.org/ko/docs/guides/anatomy-of-an-http-transaction/
먼저 위 사이트를 열심히 참고하며 따라해봤다.
클라이언트쪽에서는 작성된 문자열을 JSON 형태로 바꾸어 보내주기때문에 서버에서는 이를 다시 가공할 수 있는 문자열로 바꿀 필요가 있었다.
위 문서에 너무나도 친절하게 내용이 나와있었다.

let body = [];
request.on("data", (chunk) => {
  body.push(chunk);
}).on("end", () => {
  body = Buffer.concat(body).toString();
});

일단 위 과정을 통해 우리가 가공할 데이터를 문자열로 변환하였다. .on이 반복되며 이어지는 것이 Promise를 써서 .then으로 이어가는 것과 같은 방식으로 보여진다. (뇌피셜)

그 다음으로는 응답을 보내주기 위해 또 열심히 문서를 보며 따라해보았다.

const { headers, method, url } = request;
// 중략
response.statusCode = 200;
response.setHeader("Content-Type", "application/json");
// response.writeHead(200, {"Content-Type": "application/json"});
// 한 줄로 표현 가능

const responseBody = { headers, method, url, body };
response.write(JSON.stringify(responseBody));
response.end();

그런데 이렇게 응답을 보내줬더니 보냈던 문자열이 그대로 출력될 것이라 생각했던 자리에
[object Object] 라고 출력되어버렸다.
띠용 하며 콘솔로그를 찍어보니 단순히 그냥 responseBody가 객체여서 그런거였다.ㅋㅋ;
body에 담은 내용만 보내주면 되는구나 싶어서

response.write(JSON.stringify(body);

이렇게 바꾸고 했더니 제대로 작동했다. 제대로 응답이 가는 것을 확인한 뒤에는
body = body.toUpperCase() 를 사용해 대문자로 바꿔 보내주는 것까지 완료했다.
이제는 method와 url을 통해 대문자로 바꿀것인지, 소문자로 바꿀것인지 분기하고 그 외의 경우에는 모두 에러로 처리하는 작업을 해주었다.
아, 그 전에 CORS 에 대한 문제가 발생하지 않도록 하기 위해 먼저 해준 것이 있다.

if (method === "OPTIONS") {
  response.writeHead(200, defaultCorsHeader);
  response.end();
}

뭔가 요청이 들어오면 가장 먼저 위 조건문을 거치며 도메인, 메소드 등을 허용하는 작업을 해주고 나서, 허락을 받은 클라이언트쪽에서 그제서야 POST 요청이 들어오게 된다.

다시 적어보면,
1. 클라이언트가 요청을 보냄 (ex. POST)
2. 서버 측에서 보면 method가 POST가 아닌 OPTIONS가 먼저 온다.
3. 이에 대한 응답으로 CORS를 보낸다. 도메인, 메소드 등 허용
4. 허락을 받은 클라이언트는 원래 보내려던 POST를 그제서야 보낸다.
5. POST를 받은 서버는 이에 대한 응답을 보내준다.
이런 식이다. 그리고 CORS는 Max-Age에 써준 시간만큼 지나면 다시 또 보내줘야한다.
즉, 여기서는 처음 OPTIONS 메소드를 받고나서 10초가 지나고 또 요청을 하면 OPTIONS가 먼저 오게 된다. 그 사이에는 POST 메소드가 바로바로 온다.

또 몇 번의 시행착오 끝에 알게된 점은 response.end()가 .on("end", () => {}); 의 함수 안에 있어야 한다는 점.. 정황상 "end" 안에서 응답을 되돌려줘야하는 것 같다.

그렇게 해서 완성된 코드는 아래와 같다.

const http = require("http");

const PORT = 4999;

const ip = "localhost";

const server = http.createServer((request, response) => {
  const { headers, method, url } = request;
  if (method === "OPTIONS") {
    response.writeHead(200, defaultCorsHeader);
    response.end();
  }
  if (method === "POST") {
    let body = [];
    request
      .on("data", (chunk) => {
        body.push(chunk);
      })
      .on("end", () => {
        body = Buffer.concat(body).toString();
        if (url === "/upper") {
          body = body.toUpperCase();
        } else if (url === "/lower") {
          body = body.toLowerCase();
        }
        response.writeHead(200, defaultCorsHeader);
        response.write(JSON.stringify(body));
        response.end();
      });
  } else {
    response.statusCode = 400;
    response.end();
  }

  console.log(
    `http request method is ${request.method}, url is ${request.url}`
  );
});

server.listen(PORT, ip, () => {
  console.log(`http server listen on ${ip}:${PORT}`);
});
const defaultCorsHeader = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type, Accept",
  "Access-Control-Max-Age": 10, 
};

사실 페어분과 했을 때는 내용을 이해하느라 코드가 좀 더 정신 없었는데 혼자 다시 공부하며 조금이나마 정리를 해보았다.

1차 목표를 달성하고 나서는 페어분과 값들을 바꿔보며 실험을 조금 해보았다.
먼저 defaultCorsHeader에서 POST를 빼버리고 POST 요청을 해보았다. 우리는 당연히 이러면 요청이 되지 않을 줄 알았는데 너무 잘되는 것이었다. '이게 왜 돼?' 이러면서 열심히 구글링을 한 결과, GET, POST같은 가장 기본적인 메소드는 그냥 기본적으로 사용할 수 있도록 허용이 되어있는 모양이었다. 기본적으로 인증이 된 사용자만 할 수 있는 요청이기 때문이라나 뭐라나..

그 다음에 한 것은 npx serve client/ 를 통해 실행한 클라이언트 (포트 3000)와 기존 index.html을 통해 실행한 클라이언트(포트 5500)를 가지고 실험을 해보았다.
Origin에 3000에서만 요청이 가능하도록 명시를 해두고 테스트를 해보았더니 이번에는 예상대로 작동했다. 3000에서는 정상적으로 변환에 성공한 반면엔 5500에서는 시뻘건 CORS 에러를 띄워주었다.

express로 리팩토링한 내용도 쓰려했는데 너무 길어보여서 나눠서 써야겠다.

profile
개쩌는 개발자가 되고 싶다 !

0개의 댓글