
- #http
HTTP - 쿠키
Prolip
2025-02-13
웹 어플리케이션에서 상태를 위해 사용하는 쿠키 알아보기, 다양한 쿠키 디렉티브 알아보기
쿠키
이전 장에서 HTTP의 흐름을 정리했다. 크게 클라이언트의 요청으로 시작해 서버의 응답으로 마쳤는데, 생각해보니 서버는 클라이언트가 이전에 요청을 했던 클라이언트인지 판단할 수 있을까?
같은 클라이언트가 다시 요청하더라도 서버는 이전에 요청했던 클라이언트인지 알 수 없다. 이전에 살펴봤던 요청 헤더, 요청 본문 어디에도 클라언트를 식별할 정보가 없었다.
User-Agent 따위의 정보가 아니라 지금까지 요청을 했던 클라이언트인지에 대한 정보를 의미하는 것이다. 이는 결국 같은 클라이언트가 다시 요청하더라도 서버는 판단하지 못한다는 의미이다.
이를 HTTP가 상태가 없다고도 말하는데, 이 무상태가 유리한 상황도 있다. 바로 확장에 유리하다는 점인데, 예를 들어 작은 단위의 서비스를 운영하던 서버 한 대가 있다고 가정해보자.
이후에 갑자기 인기가 많아져 트래픽이 상승해 한 대로 감당하기 어려운 상황에서 서버를 여러 대로 늘려 트래픽을 분산할 수 있다. 이 때 늘어난 여러 서버는 한 대일 때처럼 어떤 요청이던 처리가 가능하다. 이유는 요청을 보낸 클라이언트를 기억할 필요가 없기 때문이다.
하지만 우리가 웹 어플리케이션을 이용할 때는 상태가 필요하다. 우리가 재방문으로 달라지는 로직이 있다면, 우리가 이전에 요청했던 클라이언트인지에 대한 정보를 요청 어딘가에 표시해야 될 것이다.
이 때 사용 가능한 헤더가 set-cookie 응답 헤더다. 서버에서 set-cookie라는 응답 헤더에 키/값 형태로 문자열을 지정할 수 있다.
<< Set-Cookie: sid=1
서버가 클라이언트의 요청에 응답할 때, 응답 헤더에 이 set-cookie 헤더를 등록한다면 응답을 받은 클라이언트 측의 브라우저는 set-cookie 헤더를 보고 ‘아 서버가 쿠키 저장소에 쿠키를 저장하라고 하는구나’ 하고 쿠키 저장소에 값을 저장한다.
>> Cookie: sid=1
이후에 동일한 도메인으로 브라우저에서 요청을 할 때, 위처럼 브라우저는 cookie 헤더에 쿠키 저장소에 기록된 쿠키를 요청 헤더에 포함해 전송한다.
그럼 이 서버는 요청 헤더의 쿠키에 저장된 값을 보고, 재방문 여부를 판단할 수 있게 되는 것이다.
유효 범위
위에서 잠깐 언급했듯, Set-Cookie 응답 헤더를 받은 브라우저가 쿠키를 저장한 뒤, 이후 모든 HTTP 요청에서 저장한 쿠키 값을 보내지는 않는다. 기본적으로 같은 도메인으로 요청할 때만 쿠키를 전달한다.
클라이언트를 식별하기 위해 사용한 쿠키를 브라우저가 민감하게 다루기 때문에 도메인을 한정한 것이다.
그럼 로그인한 도메인에서 받은 쿠키를 다른 도메인에서도 이용하고 싶으면 어떡하지?
Domain
Domain은 쿠키를 공유할 도메인을 지정할 수 있는 디렉티브로 서버에서 Set-Cookie 헤더를 보낼 때, 쿠키 값 뿐만 아니라 브라우저에 명시한 도메인에 요청할 때 쿠키를 전달하라고 알려줄 수 있다.
res.setHeader("Set-Cookie", "sid=1; Domain=mysite.com;");
node의 경우 setHeader에 기존의 쿠키 값을 세미콜론으로 구분해, key/value 형식으로 입력할 수 있다. 로그인 도메인이 login.mysite.com 이라고 가정해보자.
응답 헤더의 Set-Cookie 헤더를 확인해보면, Domain 디렉티브가 mysite.com으로 담겨오는 것을 확인할 수 있다. 다시 새로고침을 해보면,
요청 헤더의 Cookie 헤더에 이전에 응답 헤더로 받은 쿠키를 실어 보내고 있음을 알 수 있다. 그럼 mysite.com으로 접속하면 브라우저가 쿠키를 정상적으로 실어 보낼지 확인해보자.
이렇게 브라우저는 login.mysite.com 뿐만 아니라 mysite.com 도메인으로 HTTP 요청을 보낼 때, 쿠키를 보내게 된다. login.mysite.com에서 인증 정보를 쿠키로 받고 이후에 mysite.com에 요청할 때 쿠키에 있는 인증 정보를 Cookie 헤더에 실어 보내 자동으로 인증 정보를 공유할 수 있다.
Path
웹 어플리케이션의 경우 하위 경로에 대한 라우트가 구분되어있을 수도 있다. 예를 들어, mysite.com이라는 서비스 도메인에서 /public 경로는 인증 없이, /mypage는 쿠키로 인증이 필요한 경우 어떻게 구현할 수 있을까? Path라는 디렉티브를 이용할 수 있다.
res.setHeader("Set-Cookie", "sid=1; Path=/private");
위와 동일하게 Path 디렉티브도 key/value 형식으로 전달한다.
응답 헤더의 Set-Cookie 헤더에 Path 디렉티브가 적용되어있다. 이 경우 ‘/private’ 경로를 제외한 모든 경로에 대해서 브라우저는 쿠키를 실어 보내지 않는다.
하지만 디렉티브에 명시한 ‘/private’ 경로는 요청 헤더에 쿠키를 실어 보내게 된다.
생명주기
지금 설정된 쿠키의 경우 브라우저를 껐다가 다시 켰을 때 유지되지 않는다. 브라우저는 종료될 때 쿠키를 삭제하기 때문이다.
다시 실행하고 mysite.com에 요청을 보내면 브라우저는 요청 헤더에 쿠키를 실지 않는다. 위에서 설명한대로 쿠키를 삭제해버렸기 때문이다.
서버와 브라우저의 연결을 세션이라고 부른다. 이 세션과 같은 수명의 쿠키를 세션 쿠키라고 부른다. 브라우저를 종료할 때 서버와 브라우저의 연결인 세션이 끊기듯 브라우저를 종료할 때 쿠키도 사라지니 말이다.
그런데 브라우저가 종료되더라도 쿠키를 유지하고 싶을 수 있다. 만약 쿠키를 사용해 로그인을 구현했다고 가정해보자. 인증 후에 세션 아이디를 쿠키로 받아 브라우저에 저장했을 때, 브라우저는 종료 시점에 쿠키를 삭제해버린다.
그럼 이후에 브라우저를 다시 실행해 서버에 요청을 보낸다면 쿠키가 없어 다시 인증이 필요할 것이다.
아래에서 설명할 Max-Age와 Expires 디렉티브는 이런 상황에서 사용 가능한 유용한 디렉티브로 이 두 개의 디렉티브를 사용하지 않으면, 쿠키는 기본적으로 세션 쿠키로 동작하게 된다.
Max-Age
이런 상황에 사용할 수 있는 디렉티브인 Max-Age는 초 단위로 직접 쿠키의 수명을 지정할 수 있다.
res.setHeader("Set-Cookie", "sid=1; Max-Age=600");
위의 경우 10분간 이 쿠키를 재사용하겠다고 지정한 셈이다.
실제로 브라우저를 확인해보면 43분에 생성한 쿠키가 53분까지 유지되어 브라우저를 재실행했을 때 요청 헤더에 쿠키를 실어 보내고 있음을 알 수 있다.
Expires
Max-Age와는 사용 방법이 조금 다른지만 동일하게 쿠키의 생명주기를 관리하는 디렉티브다. Max-Age는 상대적인 시간으로 초 단위로 만료 시간을 지정한 것과 다르게 Expires는 만료 시간을 정확한 날짜 문자열로 설정한다.
const threeDaysLater = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toUTCString(); res.setHeader("Set-Cookie", `sid=1; Expires=${threeDaysLater};`);
위의 경우 3일 뒤를 만료일로 지정하고 싶을 때를 가정한 코드로 new Date로 날짜 형식의 문자열 데이터를 생성한다. Date.now() 메서드를 호출해 현재 시간을 밀리초 단위로 가져온다.
이후 자바스크립트의 Date 객체는 시간을 밀리초 단위로 다루기 때문에 3(3일) * 24(하루는 24시간) * 60(1시간은 60분) * 60(1분은 60초) * 1000(1초는 1000ms)를 더해준다.
이후 HTTP Set-Cookie 헤더의 Expires 디렉티브가 UTC 문자열 형식이어야 하기 때문에 .toUTCString() 메서드를 호출해 세계 표준시로 설정한다.
이제 서버에서 받은 쿠키가 브라우저에 3일의 유효기간을 가진채 저장된다.
Max-Age와 Expires
그런데 척 봐도 Max-Age가 더 좋아보인다. 아무래도 상대적인 시간을 설정하는 면에서 더 직관적이고 설정하기 편하다는 장점이 있지 않은가? 브라우저도 Max-Age 디렉티브를 보고 알아서 만료 시간을 계산해 적용해준다.
하지만 명확하게 만료 날짜를 지정한다던가 지금은 그런 경우가 있나 싶은데, 혹시라도 Max-Age를 지원하지 않을 경우에 사용할 수 있을듯하다.
Secure
쿠키는 본래 HTTP 헤더에 포함되어 전송된다. 이 때 암호회되지 않고 평문으로 전달된다. 즉 HTTP 환경에서 누군가 네트워크 패킷을 가로채면 쿠키를 그대로 열어볼 수 있게 된다. 이를 스니핑이라고 한다.
그럼 공격자가 네트워크 트래픽을 가로채서 쿠키 값을 읽을 수 있고, 이 쿠키에 세션 ID나 중요한 정보가 있다면 탈취해 중간자 공격이 가능해진다.
HTTPS는 전송 계층 보안.. 줄여서 TLS를 적용한 HTTP 프로토콜로 모든 HTTP 요청과 응답이 암호화되기 때문에 가로채더라도 내용을 해석할 수 없게 된다.
이걸 설명한 이유는 이 Secure 디렉티브를 사용하면 쿠키를 HTTPS 요청간에만 실어 보내도록 지시할 수 있기 때문이다.
res.setHeader("Set-Cookie", "sid=1; Secure;");
이렇게 Set-Cookie 헤더에 지정한 경우 어떻게 동작하는지 사진으로 확인해보자.
처음 도메인에 접속할 때, 응답 헤더의 Set-Cookie 헤더에 Secure 디렉티브가 지정된 채로 도착한다.
이후에 다시 접속해도 요청 헤더에 쿠키를 실어 보내지 않는다.
Secure 속성이 있는 쿠키는 HTTPS에서만 동작하기 때문에 일반 HTTP 환경에선 브라우저가 저장조차 하지 않는다. 그래서 위에서 보듯 Set-Cookie 헤더에 포함되어 응답에 오더라도 브라우저에 저장되지 않고 다시 전송되지도 않는다.
이렇게 document로 조회하려고 해도 브라우저에 저장되지 않아 조회할 수 없다. 이제 https 서버로 실행한 경우를 보자.
동일하게 응답 헤더에 Secure 디렉티브가 적용된 쿠키가 도착했다. 재접속한 경우를 보자.
이번엔 정상적으로 요청 헤더에 쿠키를 실어 보내고 있음을 알 수 있다. 그럼 위에서 실패했던 쿠키 조회를 다시 해보자.
조회도 가능하고 값을 바꾸는 행위도 가능하다. 어라? 그럼 쿠키의 위변조가 가능한 것이 아닌가? 다시 요청을 보내보면 아래와 같이 직접 바꾼 쿠키가 그대로 전송됨을 알 수 있다.
HttpOnly
위의 경우에서 볼 수 있듯 자바스크립트로 쿠키를 바꾸는 행위가 가능했다. 하지만 HttpOnly 디렉티브를 사용하면 쿠키를 HTTP 요청간에만 사용 가능하게 바꿀 수 있다. 즉, 왔다 갔다하는 행위에만 사용할 수 있고 자바스크립트의 접근 자체를 막아버릴 수 있다.
res.setHeader("Set-Cookie", "sid=1; Secure; HttpOnly;");
이렇게 HttpOnly 디렉티브를 추가하고 다시 쿠키를 조회해보자.
응답 헤더에 HttpOnly 속성이 붙어 전달됐다. 이제 다시 자바스크립트로 접근해보자.
이렇게 조회조차 불가능하며 직접 수정하려고 시도해도 불가능해짐을 알 수 있다.
정리
어쨌든 서버가 요청간에 마치 스티커를 붙여 클라이언트에게 부착된 스티커를 확인하는 메커니즘이라고 볼 수 있다. 쿠키는 key=value 형태로 만들 수 있었다.
또, 브라우저가 이 쿠키를 다루기 위한 정책들을 디렉티브로 명시했다.
- 유효범위
- Domain: 브라우저는 기본적으로 같은 도메인에 한해 쿠키를 전송하는데, Domain 디렉티브를 사용해 다른 도메인에서 사용될 수 있도록 허용할 수 있다.
- Path: 쿠키가 다른 특정 경로 요청에만 사용되도록 제어할 수 있다.
- 생명주기
- 서버와 브라우저의 연결을 세션이라고 부른다. 이 세션과 같은 수명의 쿠키를 세션 쿠키라고 부른다. 세션이 종료되어도 쿠키를 유지할 수 있는데 이를 영속적인 쿠키라고 부른다.
- Max-Age: 초 단위로 직접 쿠키의 수명을 지정할 수 있으며 상대적인 시간을 지정해 브라우저가 알아서 계산해 쿠키의 유효 시간을 지정한다.
- Expires: 만료 시간을 정확한 날짜 문자열로 설정한다. HTTP Set-Cookie 헤더의 Expires 디렉티브가 UTC 문자열 형식으로 입력해야함을 유의한다.
- 보안
- 쿠키는 평문으로 전송되기 때문에 유출될 수 있다. HTTPS의 경우 보안계층에 의해 암호화 되어 유출되어도 읽을 수 없게 암호화된다.
- Secure: Secure 디렉티브를 사용하면 쿠키가 HTTPS에서만 전송되도록 제한할 수 있다. 이 때 HTTP로 전송된 쿠키는 브라우저에 저장되지 않는다.
- HttpOnly: 쿠키가 오직 HTTP 통신에만 사용되도록 제한할 수 있다. 즉, 자바스크립트로 접근해 조회 및 수정이 불가능하게 막을 수 있다.

HTTP - 컨텐츠 협상
