[Node.js] express로 다양한 Request 파싱하기

zxcev·2023년 1월 18일
0

Node.js

목록 보기
8/17
post-thumbnail

앞서 Node.js의 기본 http 모듈을 사용하면서 다양한 종류의 Request를 파싱해 보았다.

지금까지 했던 것들을 전부 모아서 express에서는 어떻게 하는지, 간단하게 살펴보도록 하자.

1. QueryString

http://localhost:3000/?a=1&b=2
URL 맨 뒤의 ? 이후에 붙는 key=value 형태의 문자열을 일컫는다.
아래와 같이 파싱했었다.

	const qs = req.url.split('?')[1]; // a=1&b=2
	const pairs = qs.split('&'); // ['a=1', 'b=2']
	
	pairs.forEach(pair => {
		const [k, v] = pair.split('=');
      
      	// a 1
      	// b 2
      	console.log(k, v);
    });

문자열을 직접 ? 기준으로 나누고, 다시 & 기준으로 나누고, 다시 =로 나눠야 비로소 Key와 Value를 얻을 수 있었다.

// 1. parse querystring
app.get('/querystring', (req, res) => {
    const { value } = req.query;
    res.setHeader('Content-Type', 'text/html');
    res.status(200).send(`<h1>querystring value: ${value}</h1>`);
});

express는 이런 귀찮은 작업을 대신 다 해서, QueryString을 자동으로 파싱하여 req.query에 넣어준다.

GET /querystring?value=111로 요청을 보내면 req.query.value111이 될 것이다.
주의할 점은 파싱은 해주지만 무조건 문자열이기 때문에 number, boolean 등으로의 변환은 직접 해야 한다는 것이다.

전체 코드를 실행한 뒤, 위 URL로 요청을 보내면 정상적으로 동작하는 것을 확인할 수 있다.
(전체 코드는 포스트 맨 아래에 첨부 되어있다.)


2. Route Parameter

URI에 동적으로 변하는 인자를 전달하는 방법이다.
GET /:value 혹은 GET /{value}로 동적으로 변하는 부분을 표기한다.

Node.js에서 GET /params/:value로 요청을 보낼 때, value 부분을 파싱하는 방법이다.

// GET /params/VALUE
const splitted = req.url.split('/'); // ['', 'params', 'VALUE']
const path = splitted[1]; // 'params'
const param = splitted[2]; // 'VALUE'

위 코드는 URL이 짧아서 간단하지만, /a/b/c/d/e/:value와 같이 URL이 길어지게 된다면 파싱 하는 것이 상당히 귀찮아 질 것이다.

express는 이런 귀찮음을 대신 떠안아준다.

// 2. parse route parameter
app.get('/params/:value', (req, res) => {
    const { value } = req.params;
    res.setHeader('Content-Type', 'text/html');
    res.status(200).send(`<h1>route params value: ${value}</h1>`);
});

req.params에 모든 Route Parameter를 자동으로 파싱하여 저장한다.

요청을 보내면 이번에도 역시 잘 동작하는 것을 볼 수 있다.


3. x-www-form-urlencoded

사용자가 <form>의 다양한 <input>에 입력한 데이터를 Request Body에 담아, 서버에서 수신하는 방식이었다.

여기서부터는 Node.js로 처리할 때, 코드가 약간 더 길어졌었다.

let buf = [];
req.on('data', chunk => {
	buf.push(chunk);
});

req.on('end', () => {
	console.log(Buffer.concat(buf).toString());
    // need parsing process like QueryString
	res.end('ok');
});

위와 같이 데이터를 스트림으로 수신하여 하나의 Buffer로 병합한 뒤, 문자열로 변경한 다음, QueryString과 마찬가지로 파싱을 해줘야 하기 때문이다.

파싱 과정은 생략하였지만, 그 부분이 추가되면 코드는 더 길어질 것이다.

express에서는 이 과정도 매우 대신 처리해주기 때문에 매우 간단해진다.
또한 간단하지만 추가적인 처리 과정이 하나 필요하다.

app.use(express.urlencoded({ extended: true }));

위 코드를 삽입하면 된다.
이 코드가 클라이언트로부터 수신한 form을 JS 객체로 파싱해주는 역할을 한다.
원래는 a=1&b=2&c=3 형태로 들어오는 x-www-form-urlencoded 방식의 데이터를 자동으로 { a: '1', b: '2', c: '3' }처럼 JS 객체로 변환해 주는 것이다.

역시 숫자 데이터를 넣더라도 문자열로 받는다는 점을 주의하자.
extended: true라는 옵션은 파싱에 사용할 모듈을 qs로 할 것인지(true), 내장 querystring 모듈로 할 것(false)인지 정하는 것이다.
보통은 true로 설정해서 qs 모듈을 사용할 것이 권장된다.

또 하나 주의할 점은 위 코드를 라우터 위쪽에 삽입해야 한다는 것이다.
아니면 오류가 발생한다.

번지 점프를 할 때 미리 생명줄을 걸고 점프를 해야지, 뛰고 나서 생명줄을 걸려고 하면 걸 대상이 없어서 불가능할 것이다.
비슷한 논리로 코드도 위에서 아래로 실행되기 때문에 파싱하기 전에 미리 파서를 넣어줘야 제대로 동작할 수 있는 것이다.

// form html page
app.get('/form', (req, res) => {
    res.setHeader('Content-Type', 'text/html')
    res.end(`<form method="POST">
        Name: <input type="text" name="name" />
        Age: <input type="text" name="age" />
        <button>Submit</button>
    </form>`);
});

// need for parsing form
app.use(express.urlencoded({ extended: true }));
// 2. parse form
app.post('/form', (req, res) => {
    const { name, age } = req.body;
    res.setHeader('Content-Type', 'text/html');
    res.status(200).send(`<h1>name: ${name}, age: ${age}</h1>`);
});

윗부분은 클라이언트에서 form을 전송하기 위해 html을 렌더링 해주는 부분이다.
GET /form으로 접근했을 때 실행된다.

form에 데이터를 넣어 Submit 버튼을 누르면 POST /form으로 데이터가 전송되는데, 이 때 req.body에 객체 형태로 데이터가 자동으로 파싱되어 저장된다.

app.use(express.urlencoded({ extended: true }));가 반드시 라우터 위에 와야 한다는 사실을 다시 한번 상기하자.

실행 결과는 다음과 같다.

만약 순서를 옮긴다면?

// 2. parse form
app.post('/form', (req, res) => {
    const { name, age } = req.body;
    res.setHeader('Content-Type', 'text/html');
    res.status(200).send(`<h1>name: ${name}, age: ${age}</h1>`);
});
app.use(express.urlencoded({ extended: true }));


위와 같이 파서가 누락되어 req.bodyundefined가 되고, undefined.name에 접근하는 셈이 되어 오류가 발생하는 것을 볼 수 있다.


4. json

Node.js로 작성하는 부분이 form과 유사하므로 생략하겠다.
단, json 형식으로 받아온 데이터는
JSON.parse()로 문자열 -> JS 객체로 변환,
JSON.stringify()로 JS 객체 -> 문자열로 변환할 수 있으며

각 과정을 Deserialization/Serialization 혹은 Unmarshalling/Marshalling이라고 부른다는 사실을 기억하자.

특히 Deserialization/Serialization이라는 용어가 자주 사용된다.
문자열 -> JS 객체 변환이 Deserialization,
JS 객체 -> 문자열 변환이 Serialization이다.

헷갈리지 말자.

// need for parsing json
app.use(express.json());
// 3. parse json
app.get('/json', (req, res) => {
    const { name, age } = req.body;
    res.setHeader('Content-Type', 'text/html');
    res.status(200).send(`<h1>name: ${name}, age: ${age}</h1>`);
});

코드의 패턴이 form과 유사하므로 쉽게 이해할 수 있을 것이다.
app.use(express.json());를 삽입하여 json 파서를 먼저 실행한다.
역시 순서가 중요하다.

form과 마찬가지로 req.body에 json으로 받아온 데이터를 객체 형태로 파싱해서 저장해준다.

재미있는 점은 form과 달리, ""가 없는 숫자는 number 타입으로 변환된다.
"1"1을 double quotes의 유무를 통해 타입을 구분할 수 있기 때문이다.


다음은 쿠키를 생성/삭제하는 방법에 대해 알아보자.

res.writeHead(200, {
	'Set-Cookie': 'id=master',
	'Content-Type': 'text/html; charset=utf-8',
});
res.end('<h1>쿠키가 생성되었습니다.</h1>');

Node.js에서는 위와 같이 Response Header에 'Set-Cookie'라는 옵션을 Key로 주고 key=value 형태의 문자열을 Value로 지정하면 브라우저에 쿠키가 전송되었다.

쿠키에 옵션을 추가하기 위해서는 Set-Cookie: id=a3fWa; Expires=Thu, 21 Oct 2221 07:28:00 GMT; Secure; HttpOnly와 같이 문자열이 상당히 길어져야 하고, 쿠키가 여러 개인 경우를 다루진 않았지만 아래와 같은 문자열이 된다.
cookie1=1; cookie2=2; cookie3=3

쿠키가 여러 개에 각 쿠키마다 기나긴 옵션이 붙는다면 파싱하기도 상당히 귀찮아 질 것이다.
게다가 이렇게 문자열을 그대로 다루게 되면, 실수가 발생할 가능성도 점점 높아진다.

express가 자체적으로 갖고 있지는 않지만 third-party 모듈을 설치하면 쿠키도 자동으로 파싱해주기 때문에 훨씬 편하게 사용할 수 있다.

작업 중인 디렉토리 내에서 터미널을 열고 npm i cookie-parser를 입력하여 설치한다.

다음과 같이 사용할 수 있다.

const cookieParser = require('cookie-parser');

app.use(cookieParser());

app.get('/cookie', (req, res) => {
    res.cookie('id', 1, {
        // domain
        // encode
        // expires
        // httpOnly
        // maxAge
        // path
        // sameSite
        // secure
        // signed
    });
    res.setHeader('Content-Type', 'text/html');
    res.status(200).send('<h1>Cookie Generated</h1>');
});

이번에도 패턴은 똑같다.
파싱을 담당하는 파서를 불러와서 app.use()에 등록한다.
위에서 아래로 코드가 실행되기 때문에 등록한 이후에 사용 가능하다.

res.cookie()에 인자로 쿠키의 Key, Value를 전달한다.

3번째 인자로 다양한 쿠키 옵션을 줄 수 있는데, 문자열이 아닌 JS 객체라 자동 완성이 되기 때문에 옵션을 외우지 않고 뭐가 있나 볼 수 있기 때문에 훨씬 편리하다.


GET /cookie로 접속한 뒤, 크롬의 Application 탭에서 Cookies를 살펴보면 정상적으로 id라는 쿠키가 등록된 것을 볼 수 있다.

app.get('/remove-cookie', (req, res) => {
    res.clearCookie('id');
    res.setHeader('Content-Type', 'text/html');
    res.status(200).send('<h1>Cookie Cleared</h1>');
});

쿠키를 삭제하고 싶을 때는 res.clearCookie()에 인자로 쿠키의 Key를 주면 된다.
GET /remove-cookie에 접속하면 방금 생성한 쿠키가 사라질 것이다.


6. Sending File

Node.js에서 서버가 갖고 있는 html 파일 자체의 내용을 클라이언트에 보낼 때, fs 모듈의 readFile 메소드를 통해 파일을 읽어온 데이터를 보내주는 방식으로 처리했었다.

const http = require('http');
const fs = require('fs').promises;

http.createServer((req, res) => {
	fs.readFile('./index.html')
	  .then(data => {
  		res.end(data);
	});
}).listen(3000);

expressres.sendFile() 메소드 하나로 같은 기능을 구현할 수 있다.

const path = require('path');

app.get('/file', (req, res) => {
    res.sendFile(path.join(__dirname, 'index.html'));
});

path 모듈을 불러와서 사용한 이유는, OS에 따라 path의 delimeter(구분자)가 달라지기 때문이다.
Windows는 C:\dir1\dir2\dir3과 같이 \로 path가 나뉘는 반면, macOS 등의 리눅스 계열은 /dir1/dir2/dir3처럼 /로 path가 나뉜다.

그래서 실행 환경에 따라 파일을 찾지 못할 수도 있게 되는데, path.join()은 OS에 따른 path delimeter 보정을 알아서 해주기 때문에 이런 것에 신경 쓸 필요가 없어진다.

또한 __dirname은 현재 실행중인 JS 파일이 위치한 디렉토리의 path를 갖고 있다.
만약 현재 실행된 JS 파일의 path가 /Documents/dev/main.js라면 __dirname/Documents/dev가 될 것이다.

클라이언트에 보내줄 파일 이름은 index.html이므로 path.join()에 이 둘을 인자로 전달하면 /Documents/dev/index.html이 될 것이다.
경로에 따라 다르겠지만 윈도우라면 C:\Documents\dev\index.html과 비슷한 형태가 될 것이다.

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Home</title>
</head>
<body>
    Home Page
</body>
</html>

Serving 할 html 파일은 위와 같다.
이제 GET /file로 접속해보자.


정상적으로 html 내용이 렌더링 되는 것을 확인할 수 있다.

0개의 댓글