온라인 쇼핑몰에서 장바구니에 물건을 담거나, 특정 웹사이트에서 다크 모드를 설정하는 등의 행위를 하면 사용자는 다음 번에도 장바구니에 물건이 담겨있을 것이고, 다크 모드가 그대로 유지될 것이라고 예상한다.(실제로 그렇게 작동하기 때문에 경험에 의해서)
이렇게 아주 작지만 계속 기억해야 하는 데이터들은 주로 HTTP 쿠키에 저장된다.
예를 들어 구글에 접속한 뒤 크롬 개발자 도구에 들어가 Application 탭의 Cookies를 열게 되면 다음과 같은 화면이 보일 것이다.
알 수 없는 여러 개의 쿠키들이 저장되어 있는 것을 볼 수 있다.
이 쿠키들은 작지만 로그인 정보 저장 등의 매우 중요한 역할을 하기도 한다.
그래서 웹개발을 하게 된다면 반드시 사용하게 되는데, Node.js를 사용하여 쿠키를 직접 생성해보도록 하자.
코드는 매우 간단하다.
const http = require('http');
http.createServer((req, res) => {
res.writeHead(200, {
// 쿠키 생성
'Set-Cookie': 'c1=cookie1!',
'Content-Type': 'text/html; charset=utf-8',
});
res.end('<h1>쿠키가 생성되었습니다.</h1>');
}).listen(3000);
Response Header에 Set-Cookie
를 설정해주면 끝이다.
주의할 점은 key=value
형태로 값이 들어가야 한다는 것이다.
위 코드에서는 key를 c1
로, value를 cookie1!
로 주었다.
이제 서버에 접속해보면 다음과 같은 화면이 정상적으로 나타날 것이다.
그리고 아까 보았던 Application 탭의 Cookies를 살펴보면, 정상적으로 c1
이라는 쿠키가 등록된 것을 볼 수 있다.
매우 간단하다.
쿠키는 결국 작은 문자열 쪼가리일 뿐이다.(최대 크기는 4000Byte 정도 된다고 한다.)
복잡한 파싱도 필요 없고, 그냥 =
앞 뒤로 key와 value를 구분해서 브라우저에 저장해 준다고 생각하면 된다.
아까 key=value
형태로 값이 들어가는 것이 중요하다고 했는데, =
가 아닌 다른 구분자가 들어간다면 어떻게 될까?
해당 결과를 보기 위해 :
를 구분자로 서버를 살짝 수정해보았다.
const http = require('http');
http.createServer((req, res) => {
res.writeHead(200, {
'Set-Cookie': 'c2:cookie1!',
'Content-Type': 'text/html; charset=utf-8',
});
res.end('<h1>쿠키가 생성되었습니다.</h1>');
}).listen(3000);
아래는 아까 만든 쿠키, 위에가 새로 만들어진 쿠키다.
Name은 비어 있고, 모든 값이 value로 들어가 있다.
그러니까 뒤에서부터 =
를 만날 때까지의 값이 value가 되고, =
를 만나면 그 이후의 값들은 name이 되는 형태인 것으로 추측된다.
name이 비어있으면 같은 key로 인식되어 덮어 씌워지게 될테니 실제로는 이런 식으로 사용할 일은 없을 것이다.
단지 궁금했던 점을 살짝 실험해보았다.
http request도, response도 cookie도 결국 전부 문자열이고, 서버와 브라우저에 의해 정해진 규칙대로 파싱되어 각자 의미 있는 데이터 쪼가리가 될 뿐이다.
이 사실을 몰랐을 때는 모든 것이 정말 추상적으로 보이고 어렵게 느껴졌었는데, 이제는 http 통신에서 어떤 요소가 어떤 방식으로 파싱되는지를 보는 재미도 있는듯 하다.
헷갈리지 않기 위해 아까 발급한 쿠키는 모두 제거하자.
간단한 로그인을 구현한다고 가정하고, id=1
이라는 쿠키를 발급한 뒤, 이를 출력해보자.
req.headers.cookie
에서 클라이언트의 쿠키를 가져올 수 있다.
const http = require('http');
http.createServer((req, res) => {
res.writeHead(200, {
'Set-Cookie': 'id=1',
'Content-Type': 'text/html; charset=utf-8',
});
console.log(req.headers.cookie); // id=1
res.end(`<h1>User ID is ${req.headers.cookie.split('=')[1]}</h1>`);
}).listen(3000);
쿠키가 단 하나만 존재하기 때문에 id=1
형태의 문자열이므로 split('=')
이후 2번째 요소인 value를 가져와 브라우저에 렌더링해준다.
정상적으로 User ID is 1
이 렌더링되며, Application의 Cookies에도 id라는 쿠키가 잘 발급된 것을 볼 수 있다.
그런데 만약 쿠키에 보다 민감한 정보가 담겨있고, 자바스크립트에서 이 쿠키에 접근할 수 있다면?
보안 상의 헛점이 생길 수도 있을 것이다.
쿠키가 일정 시간 지난 이후, 예를 들어 1주일 뒤에 자동 삭제 된다거나 하는 요구 사항이 생길 수도 있을 것이다.
작지만 강력한 쿠키는 이런 다양한 요구 사항을 충족시킬 수 있도록 옵션을 넣을 수 있게 되어있다.
이 옵션 역시도 그저 쿠키를 생성할 때 문자열 몇 개를 추가해서 간단하게 지정할 수 있다.
아래와 같은 식이다.
Set-Cookie: id=a3fWa; Expires=Thu, 21 Oct 2221 07:28:00 GMT; Secure; HttpOnly
key는 id, value는 a3fWa인 쿠키이며, Expires는 쿠키가 언제 만료가 될 것인지, Secure는 쿠키가 네트워크 전송 중에 탈취되는 것을 막기 위해 HTTPS 통신인 경우에만 쿠키를 전달하는 옵션이다.
그리고 HttpOnly는 아까 논의한 자바스크립트에서 쿠키를 접근할 수 없도록 막는 옵션이다.
이 외에도 다양한 옵션이 있으며, express나 Spring 등의 유명한 웹 프레임워크에서는 메소드 에 인자 형태로 이런 옵션들을 넣어주면 되는데, 이 모든 것들은 결국 내부적으로는 문자열 쪼가리를 파싱하는 개념이라는 것을 알 수 있었다.
더 많은 옵션은 아래 링크에 자세하게 나와있다.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies