Skip to content

쿠키는 어떻게 웹의 기억이 되었을까

쿠키란 뭘까? 왜 존재할까?

Makonea
··53분

쿠키(Cookie) 하면 사람들은 보통 먹는 과자를 떠올린다.
안타깝게도 프로그래머라는 족속은 쿠키 하면 "브라우저에 박히는 4KB짜리 key=value"를 먼저 떠올리고, 제과점에서 "맛있는 쿠키"를 보면 "이걸로 세션 유지가 되나?" 같은 소리를 하는 병자들이다. 프로그래머가 너드(nerd)로 오해받는 이유는 여러 가지지만, 일상 단어를 멋대로 납치해 엉뚱한 뜻으로 쓰는 선배들의 전통도 단단히 한몫한다.

그럼 이쯤되면 궁금해진다. 도대체 왜 선배 프로그래머들은 도대체 왜 과자 이름을 가져다 붙였을까. 그리고 그 쿠키가 유지해준다는 '세션'은 무엇일까? 왜 과자 이름을 붙여서 후배 프로그래머가 프로그래밍 용어인 쿠키를 검색할때마다 먹는 쿠키 사진만 뜨게한걸까.

이걸 이해하려면 쿠키라는 단어를 잠시 접어두고, 먼저 상태(state) 라는 개념부터 시작해야 한다.
상태는 이전에 일어난 일이 이후의 행동을 바꾸도록 남아 있는 정보다. 상품 페이지에서 키보드를 장바구니에 넣었다면 "장바구니에 키보드가 있다"가 현재 상태이며, 결제 페이지는 이 상태를 읽어 키보드의 가격을 계산해야 한다. 로그인 여부, 작성 중인 주문, 선택한 언어도 이전의 선택이 다음 요청의 결과를 바꾼다는 점에서 상태가 된다.

상태가 메모리에 저장되든, 데이터베이스에 저장되든, 브라우저에 저장되든 그건 일단 뒷전이다. 진짜 문제는 무엇인가?
HTTP는 내가 방금 뭘 했는지 기억하지 못한다. 이전 요청의 맥락을 완벽하게 소거하는, 고도로 설계된 기억상실증을 앓고 있는 셈이다
키보드를 장바구니에 넣고 결제 페이지로 이동했는데, 서버 입장에서는 "누군가 결제 페이지를 달라고 하네?"까지만 알 뿐이다.
이게 바로 HTTP의 무상태성(statelessness)이다. 데이터베이스를 쓰지 말라는 뜻이 아니다. 그냥 모든 요청이 자기 할 말을 스스로 챙겨야 한다는 뜻이다. 이전 요청에서 "아까 그거 있잖아요" 같은 암묵적인 문맥에 기대면 안 된다.

예를 들어보자. 내가 키보드를 장바구니에 넣었다.
그리고 결제 페이지(/checkout)로 이동했다. 이때 HTTP 입장에서 이 두 요청은 그냥 남남이다. "아까 그 손님이 또 왔군요" 같은 인식은 없다. 두 번째 요청을 받은 서버는 "누군가 결제 페이지를 원하네?"는 알겠는데, 수많은 장바구니 중에 내 것을 어떻게 꺼내야 할지 모른다.

초기 웹은 이래도 괜찮았다. 문서 하나 던져주고 끝이었으니까. 그런데 로그인도 하고, 장바구니도 담고, 이전에 했던 일을 이어가야 하는 애플리케이션이 등장하면서 이 기억상실증이 문제가 됐다. 누군가는 이 간극을 메워야 했다.

그리고 이 빈틈을 메우기 위해 등장한 게 세션(session) 이다. HTTP는 여전히 아무것도 기억하지 못하지만, 애플리케이션이 "이 요청과 저 요청은 사실 같은 활동입니다"라고 우기는 셈이다.

세션은 말하자면 독립적인 HTTP 요청 여러 개를 하나의 흐름으로 묶어주는 논리적인 문맥이다. 로그인부터 로그아웃까지를 한 세션으로 삼을 수도 있고, 로그인도 안 한 채 장바구니 담고 결제까지 끝내는 과정을 한 세션으로 삼을 수도 있다. 기준은 애플리케이션이 정하기 나름이다. HTTP가 세션을 뚝딱 만들어 주는 게 아니니까, 언제 시작해서 언제 끝낼지, 그동안 뭘 기억할지는 전부 애플리케이션 몫이다. TCP 연결 하나가 세션 하나와 일치할 거라는 보장도 없고, 브라우저 탭 하나가 세션 하나일 거라는 법도 없다.

그런데 세션을 만들었다고 끝이 아니다. 서버에는 지금 이 순간에도 수많은 로그인 세션과 익명 장바구니가 뒤엉켜 있다. 새 요청이 하나 도착했다. 서버 입장에서는 "그래서 이게 누구 세션이지?"를 알아낼 방법이 필요하다. 이때 필요한 게 식별자(identifier) 다.

식별자는 말 그대로 "여러 개 중에 하나를 찍어주는 표식"일 뿐이다. 꼭 실명이어야 할 필요도 없고, 계정이어야 할 필요도 없다. 같은 사람이 브라우저랑 앱에서 각각 다른 식별자를 들고 다닐 수도 있고, 로그인도 안 한 장바구니에 식별자가 붙는 일도 아무렇지 않다. 7f41c0a9 같은 값의 뜻은 "7f41c0a9라는 사람"이 아니라 "서버야, 7f41c0a9 자리에 저장된 세션 기록 꺼내줘" 에 가깝다.

장바구니 예제로 다시 구분하면, 키보드가 들어 있다는 사실은 상태이고, 여러 요청에 걸쳐 그 장바구니를 이어서 사용하는 문맥은 세션이며, 7f41c0a9는 서버에 저장된 세션 기록을 찾는 식별자다. 이제 남은 문제는 무상태인 다음 HTTP 요청에 이 식별자를 어떻게 다시 실어 보낼지이며, 쿠키가 이 마지막 빈칸을 채운다.

Loading animated diagram...

그럼 식별자를 어떻게 매번 돌려받을 수 있을까. 서버가 값을 클라이언트한테 맡겨두고 다음 요청 때 돌려받으면 되는데, 문제는 뭘 근거로 "아까 그 클라이언트"를 알아보느냐다. IP 주소는 금방 바뀌고, 여러 사람이 같은 IP를 쓰기도 하니까 믿을 수가 없다. 연결은 끊기거나 재사용될 수 있으며, 하나의 사용자 세션과 안정적으로 일치하지 않으므로 식별 수단으로 삼을 수 없기때문이다.

결국 웹에 필요했던 것은 브라우저가 서버별 값을 자동으로 보관하고 후속 요청에 돌려주는 공통 방법이었다. 그중에서 쿠키는 간단한 방법이었다. 서버가 "이걸로 기억할게" 하고 값을 하나 발급해주고, 브라우저가 그걸 보관하고 있다가 앞으로 관련된 요청마다 자동으로 다시 보내주는 것. 이게 바로 쿠키가 등장한 이유다.

쿠키는 이 왕복 규칙을 HTTP 헤더로 구현한 장치다. 기본 꼴은 단순하다. 이름=값 하나가 전부다. 이름은 "이 쿠키가 뭐 하는 애냐"를 구분하는 라벨이고, 값은 서버가 실제로 쓸 내용이다.

방금 전 예제로 치면 이렇다고 할 수 있다.

Code
cart_id=7f41c0a9

cart_id가 이름, 7f41c0a9가 장바구니 식별자다. 맨 처음 키보드를 장바구니에 넣을 때는 아직 식별자가 없으니까, 당연히 요청에도 Cookie 헤더는 비어 있다.

HTTP
POST /cart/items HTTP/1.1
Host: shop.example
Content-Type: application/x-www-form-urlencoded

item=keyboard

서버는 이 요청을 받고 이렇게 생각한다.
"아, 새 손님이구나. 식별자부터 하나 만들어야겠다."
식별자 7f41c0a9를 만들고, 그 아래에 장바구니 상태를 저장한다. 서버 메모리 어딘가에는 이런 기록이 생긴다.

Text
cart_store["7f41c0a9"] = ["keyboard"]

그리고 응답을 돌려주면서 브라우저한테 부탁한다.

"이거 받아서 잘 보관하고 있다가, 앞으로 여기 올 때마다 다시 보내줘."

Code
HTTP/1.1 200 OK
Set-Cookie: cart_id=7f41c0a9; Path=/; HttpOnly; SameSite=Lax

브라우저는 Set-Cookie를 받으면 cart_id=7f41c0a9를 받아서 기억해두고, 적용 범위를 보관했다가,다음에 /checkout으로 갈 때는 이렇게 쿠키를 자동으로 붙여 보낸다.(즉 조건이 맞는 다음 요청에 Cookie 헤더를 붙인다.)

HTTP
GET /checkout HTTP/1.1
Host: shop.example
Cookie: cart_id=7f41c0a9

서버는 요청에서 cart_id를 발견하고 이렇게 생각한다. "아까 그 손님이구나. 7f41c0a9... 찾았다, 키보드 담아뒀던 장바구니!"

서버는 cart_id라는 이름을 보고 장바구니 식별자가 왔다는 것을 알고, 값 7f41c0a9를 이용해 앞에서 만든 세션 기록을 찾은 뒤 그 안의 장바구니 상태를 읽는다.
여기서 중요한 핵심은 하나 있다. 브라우저는 7f41c0a9라는 값이 장바구니 식별자인지, 로그인 세션인지 전혀 모른다. 그냥 이름표 보고 조건 맞으면 갖다줄 뿐이다. 이 값의 의미를 결정하는 쪽은 순전히 서버 애플리케이션이다. Path, Domain, Secure, HttpOnly, SameSite, 만료 시각 같은 속성들도 마찬가지다. 값의 뜻과는 아무 상관없이, 오직 "이 쿠키를 언제, 어디로 보낼지" 만 브라우저에게 알려주는 규칙일 뿐이다.

그런데 쿠키의 value가 항상 식별자인 건 아니다. theme=dark처럼 설정값 자체를 통째로 담을 때도 있다. 하지만 로그인이나 장바구니처럼 중요한 상태를 다룰 땐, 예측하기 어려운 식별자만 브라우저에 맡기고 실제 데이터는 서버가 쥐고 있는 쪽이 일반적이다.

브라우저에 맡긴 값은 사용자가 얼마든지 뜯어고쳐서 보낼 수 있다. 그래서 서버는 쿠키 값을 절대 '믿을 수 있는 내부 데이터'로 착각하면 안 된다. 특히 세션 식별자는 그냥 서버 쪽 기록을 찾는 열쇠일 뿐, 그 값 자체에 민감한 정보를 담지 않는 게 안전하다.1

이제 흩어져 있던 개념들을 한 번에 정리해보자.

  • 상태: 이어서 보존해야 할 정보

  • 세션: 그 상태를 여러 요청에 걸쳐 사용하는 논리적 문맥

  • 식별자: 수많은 세션 중 내 것을 찾아내는 값

  • 쿠키: 식별자나 작은 상태를 브라우저에 맡겼다가, 다음 요청에서 되돌려 받는 HTTP 상태 관리 장치

정리하면 상태는 이어서 보존해야 할 정보이고, 세션은 그 상태를 여러 요청에 걸쳐 사용하는 논리적 문맥이며, 식별자는 많은 세션 가운데 하나를 찾는 값이고, 쿠키는 그 식별자나 작은 상태를 브라우저에 보관시켰다가 다음 요청으로 되돌려 받는 HTTP 상태 관리 장치다. 현재 발행된 표준은 RFC 6265이고, 후속 문서는 RFC-to-be 10025라는 번호를 받아 RFC Editor의 최종 검토 단계에 있다.

페이지를 넘겼는데 손님이 사라졌네?

HTTP는 철처한 무상태(stateless) 프로토콜이다. RFC 9110 3.3절을 빌리자면, 각 요청 메시지의 의미를 다른 요청과 분리해서 이해할 수 있어야 한다고 설명한다. 같은 TCP 연결에 연달아 꽂혔다고 "아, 이 사람 아까 그 사람이구나" 하고 가정해선 안 되고 독립적으로 인식해야한다는 것이다. HTTP가 제정한 이 원칙하에 인터넷은 움직인다.

여기서 사람들이 자주 헷갈리는 부분이 있는데, "무상태라면서 왜 서버는 데이터베이스 쓰냐" 같은 이야기들인데, 무상태는 서버가 메모리를 못 쓰거나 DB 연결을 끊어야 한다는 뜻이 아니라, HTTP 요청이라는 녀석이, "나 지금 로그인 상태야"라는 정보를 이전 요청에서 자동으로 끌어오지 않는다는 뜻이다.

즉 무상태(stateless)는 서버가 데이터베이스나 메모리를 가져서는 안 된다는 뜻이 아니다. HTTP 요청 자체가 이전 요청의 애플리케이션 상태를 자동으로 들고 오지 않는다는 뜻에 가깝다.
연결을 오래 유지하는 keep-alive도 로그인 상태를 만들어 주지 않는다. 전화선을 끊지 않았다고 상담원이 고객의 장바구니를 알게 되는 것은 아니다.

이와 같은 요청 독립성의 장점은 REST에서도 더 강한 제약으로 정리됐는데, Fielding은 REST의 stateless 제약이 요청 사이의 상태를 서버가 보관하지 않게 해 신뢰성과 확장성을 높인다고 설명했다. 서버는 한 요청을 처리한 뒤 관련 자원을 빨리 놓을 수 있고, 일부 장애가 나도 다른 구성 요소가 요청 하나만 보고 이어받기 쉬워진다는 것이다.
사람이 방금 나눈 대화를 기억하지 못하면 병원에 가야 하지만, 서버가 같은 일을 하면 확장성이 좋아진다.

초기 웹에서는 이 성질이 별문제가 아니었다. 문서를 요청하면 서버가 문서를 보내고 일이 끝났으니까 말이다.

문제는 1994년 여름 Netscape의 쇼핑 서버 팀이 장바구니를 구현하면서 이 성질이 바로 문제가 된 것이다. 사용자가 상품 페이지에서 물건을 골랐는데 결제 페이지로 넘어가면, 다음 요청만 받은 서버에는 그 사람이 조금 전에 무엇을 골랐는지 알려주는 공통 장치가 없었다.

쿠키가 없을 때 상태를 운반하는 법

브라우저가 값을 자동으로 돌려주기 전에도 방법은 있었다. 세션 식별자를 모든 URL에 붙이는 것이다.

HTTP
GET /cart?session_id=7f41c0a9 HTTP/1.0

폼을 거칠 때는 hidden field에 넣었다.

HTML
<form action="/checkout" method="post">
  <input type="hidden" name="session_id" value="7f41c0a9">
  <button type="submit">결제</button>
</form>

이 방식은 애플리케이션이 모든 링크와 폼에 식별자를 계속 실어 보내야 한다. 중간 페이지 하나가 값을 빼먹으면 사용자는 새 사람이 된다.
OWASP가 정리하듯 URL에 넣은 세션 ID는 링크와 로그, 방문 기록, 북마크, Referer 헤더, 검색 엔진을 통해 노출될 수 있다. 세션 식별자가 인증 수단인 시스템이라면 상품 링크를 받은 사람이 로그인 상태까지 함께 받을 수 있으니, 초기 웹은 콘텐츠 공유와 권한 위임을 URL 하나로 처리하는 매우 급진적인 협업 기능을 제공했던 셈이다. 이렇게 설명하면 와닿지 않을테니 아주 직관적으로 이해하기 쉽게 문제를 이해하자면, 이 방식을 쓴 사이트에서 로그인한 채로 친구에게 링크를 복사해 보냈다고 치자.

친구는 내 장바구니뿐 아니라 내 구매 내역, 그리고 어쩌면 내가 뜨거운 밤을 보내기 위해 구비해놓은 부끄러운 취향의 성인용품 목록까지 함께 열람할 수 있다. 물론 남성 스판덱스 퍼리슈트는 부끄러운 취향이 아니지만, 그렇다고 당당하게 공개할 취향도 아니다.

URL에 식별자를 붙이는 방식에는 또 하나의 골칫거리가 생기는데, 바로 캐시다.

웹에서 캐시는 같은 리소스를 여러 번 다운로드하지 않도록 중간에 저장해두는 장치다. 그런데 캐시가 같은 문서인지 아닌지 판단하는 기준은 URL이다. URL이 다르면 내용이 똑같아도 다른 문서라고 판단한다.

여기에 session_id=7f41c0a9 같은 값이 붙기 시작하면 문제가 생긴다. 사용자마다 URL이 달라지니까, 모두에게 똑같이 보여줘도 되는 /product/keyboard 페이지임에도 캐시는 각 URL을 전부 다른 문서로 인식한다. 같은 상품 페이지가 사용자 수만큼 캐시 저장소에 중복 저장되는 셈인데, 캐시 효율은 바닥을 치고, CDN도 제 역할을 못 한다.

RFC 2964가 세션 상태를 URL에서 분리하라고 권고한 이유 중 하나도 바로 이것이다.2캐시가 같은 리소스의 사본을 여러 개 붙들고 낭비하지 않게 하기 위해서다.

거기에 사용자 경험 문제도 같이 발생한다. 페이지를 오가고, 폼을 작성하고, 링크를 클릭하는 상호작용이 늘어날수록, 애플리케이션은 그 모든 경로에 식별자를 끼워 넣어야 한다. 실수로 하나라도 빠지는 순간 사용자는 갑자기 로그아웃된 것처럼 보이거나 장바구니가 텅 비어 보인다. 그리고 당시 끔찍한 개발환경을 고려한다면 이런 실수는 꽤 자주 발생했을 것이다.
개발자 입장에서는 모든 템플릿과 리다이렉트 로직에 식별자 주입 코드를 심어야 하는 지옥의 단순 노동이고, 사용자 입장에서는 아무 이유 없이 상태가 증발하는 미스터리를 겪는 셈이다.

결국 모두가 원한 건 단순한 것이다. 브라우저가 서버별로 작은 값을 알아서 기억하고, 해당 서버에 요청할 때마다 자동으로 실어 보내주는 규칙. 개발자는 링크마다 식별자를 붙일 필요가 없어지고, 사용자는 주소창을 복사해도 내 세션이 통째로 따라붙지 않고, 캐시는 URL을 정상적으로 사용할 수 있는, 그러니까 지금 생각하면 당연한, 그때는 당연하지 않았던 바로 그 메커니즘 말이다.

1994년, Netscape가 브라우저에 메모를 맡기다

Lou Montulli의 2013년 회고에 따르면 1994년 7월쯤 Netscape의 다른 팀이 쇼핑 서버와 장바구니 문제를 들고 왔는데, 당시에는 브라우저마다 영구 식별자를 넣자는 제안도 돌고 있었다. 어느 사이트에서든 같은 식별자를 볼 수 있는 구조였고, Montulli는 사이트 간 추적에 쓰일 수 있어 반대했다.

그가 생각한 방식은 서버가 값을 하나 만들고, 브라우저가 그 값을 해당 사이트로만 돌려보내게 하는 것이었다. 처음에는 세션 식별자가 중심이었지만 곧 일반적인 작은 페이로드(payload)를 전달하는 방식으로 넓어졌다.

Netscape 브라우저는 1994년 가을에 이 기능을 담아 배포됐고 다른 브라우저와 웹사이트도 구현할 수 있도록 비공식 쿠키 명세가 공개됐다. 당시의 기본 교환은 지금과 거의 같다.

실제 교환은 놀라울 만큼 단순한데, 서버는 응답 헤더에 이렇게 한 줄 실어 보낸다.

HTTP
HTTP/1.0 200 OK
Set-Cookie: session_id=7f41c0a9; Path=/

"야, 이 값 session_id=7f41c0a9 좀 기억해둬. 그리고 앞으로 / 아래로 오는 요청에는 이거 꼭 붙여서 보내." 브라우저는 이 지시를 받아서 값을 저장해둔다.
사용자는 아무것도 모른다. 그냥 평소처럼 링크를 클릭할 뿐이며, 대신 이번엔 브라우저가 조용히 뒤에서 쿠키를 붙여 보낸다.

HTTP
GET /checkout HTTP/1.0
Cookie: session_id=7f41c0a9

서버는 요청에서 session_id를 꺼내고, 자기 저장소를 뒤져서 이 식별자에 해당하는 세션 데이터를 찾는다. 메모리 어딘가에는 이런 기록이 들어 있다.

Text
session_store["7f41c0a9"] = {
    user_id: 42,
    cart: ["book", "keyboard"]
}

이제 서버는 "아까 그 사람이구나" 하고 장바구니를 이어서 보여줄 수 있다. 사용자는 링크를 복사해서 보내도 세션이 따라붙지 않고, URL마다 식별자를 수동으로 붙일 필요도 없고, 캐시는 URL을 정상적으로 쓸 수 있다.

참고로 예시에 쓴 7f41c0a9는 설명을 위해 짧게 지은 값이다. (나의 유일한 친구인 Mr.ChatGPT에게 대충 예시 코드 몇개 던져보라고 하니까 던져준 값이니까 너무 의미를 두지말아줬으면 한다.)
실제 서비스에서는 절대 이렇게 짧은 식별자를 쓰면 안 된다. 공격자가 때려맞추기 어렵도록 충분히 길고 예측 불가능한 난수를 써야 하며, 그렇지 않으면 다른 사용자의 세션을 추측해서 가로챌 수 있고, 그건 곧 계정 탈취로 이어진다.

쿠키에 장바구니 전체를 넣을 수도 있지만 흔한 서버 세션 방식에서는 예측하기 어려운 식별자만 넣는다. 로그인 사용자와 장바구니의 원본은 서버가 소유한다. 브라우저는 식별자를 운반한다.

그래서 누누이 강조하지만 쿠키와 세션은 같은 말이 아니다. 쿠키는 브라우저가 보관하고 전송하는 값과 규칙이고, 세션은 여러 요청을 하나의 사용자 맥락으로 묶는 애플리케이션 개념이다. 쿠키 없이 세션을 만들 수도 있고, 세션이 아닌 언어 설정을 쿠키에 담을 수도 있다.

그런데 왜 이름이 쿠키(cookie)였나

그런데 도대체 왜 하필 '쿠키(Cookie)'라는 이름이 붙었을까. 먹는 과자와 아무 상관없는 이 기술에, 선배 프로그래머들은 대체 무슨 생각으로 그 이름을 가져다 붙인 걸까.

짜잔! 놀랍게도 이 질문의 답은 모른다이다.

정확히는 웹 쿠키의 이름이 어디에서 왔는지는 알지만, 그보다 앞선 컴퓨팅 용어인 magic cookie가 왜 처음부터 cookie라고 불렸는지는 분명하지 않다.

웹 쿠키 이름의 직접적인 선행 용어로 확인되는 것은 magic cookie다. Magic cookie는 한 프로그램이 다른 프로그램에 건네는 불투명한 값이다. 값을 받은 쪽은 그 안의 구조나 의미를 해석하려 하지 않고, 그냥 보관했다가 나중에 발행한 쪽에 그대로 돌려준다.

"너는 이게 무슨 뜻인지 몰라도 돼. 그냥 잘 간직하고 있다가 내가 필요할 때 다시 내놔."

이는 특히 불투명한 세션 식별자를 브라우저에 맡겼다가 다음 HTTP 요청에서 돌려받는 웹 쿠키의 사용 방식과 매우 닮았다. 다만 모든 웹 쿠키가 불투명한 값인 것은 아니다. theme=dark처럼 의미가 드러나는 작은 상태를 직접 담을 수도 있다.

이 용어가 실제로 얼마나 오래됐는지는 1979년 UNIX V7 Programmer's Manual의 fseek 설명에서 확인할 수 있다.
이 매뉴얼은 ftell()이 반환하는 파일 위치값이 UNIX에서는 바이트 단위로 측정되지만, 다른 시스템에서는 magic cookie일 수 있다고 설명한다. 그런 값은 숫자처럼 해석하거나 계산하지 말고, 나중에 파일 위치를 복원할 때 fseek()에 그대로 돌려줘야 한다.3
이미 이때부터 '받는 놈은 궁금해하지 말고 그냥 돌려주는 값'이라는 개념이 '쿠키'라는 이름으로 불리고 있었던 셈이다.
오늘날의 C 형태로 뜻만 옮기면 다음과 비슷하다.

C
long cookie = ftell(stream);

/* 다른 읽기 작업 */

fseek(stream, cookie, SEEK_SET);

여기서 cookie라는 변수명은 설명을 위해 붙인 것인데, 핵심은 cookie + 1이 반드시 다음 파일 위치를 뜻하지는 않는다는 데 있다.
그 값의 내부 의미를 아는 척하지 말고, 발급한 API에 그대로 돌려줘야 한다.

적어도 1979년의 매뉴얼에서는 이런 값을 이미 magic cookie라고 부르고 있었다. 현재의 Jargon File 역시 이를 내부 구조가 정의되지 않은 채 나중에 같은 프로그램이나 다른 프로그램에 다시 전달하는 opaque identifier로 설명한다4

당시 매뉴얼을 쓰던 사람들도 이 값을 가리켜 'magic cookie'라고 부르고 있었고, 그 이름은 이미 "의미는 묻지 말고 보관했다가 그대로 돌려주는 불투명한 값"이라는 컴퓨팅 세계의 관습을 반영하고 있었다. HTTP 쿠키가 등장하기 최소 15년 전의 이야기다.

Montulli는 대학 운영체제 수업에서 들은 magic cookie를 떠올렸고 웹의 동작과 비슷해서 cookie라는 이름을 골랐다고 썼다. 2022년 AMA에서는 오래된 운영체제 매뉴얼에서 본 표현과, 과자 안에 작은 메시지가 들어 있는 포츈 쿠키 (fortune cookie)의 이미지도 자신의 작명에 영향을 주었다고 설명했다.

그가 읽었다는 매뉴얼의 이름은 자세히 역사에 남지 않았지만, 어쨌건 웹에서 쓰이는 쿠키는 Magic Cookie에서 파생되었고, 그 Magic Cookie가 왜 쿠키로 이름지어졌는지는 아무도 모른다.

구현이 먼저, 표준은 나중에

프로그래밍 세계의 수많은 명세가 그렇듯, 쿠키도 표준보다 구현이 먼저였다. 누군가 회의실에서 완벽한 명세를 디자인한 다음 브라우저에 심은 게 아니다. Netscape가 1994년에 "일단 이렇게 하자" 하고 비공식 명세를 만들고 브라우저에 넣어버렸다. HTTP/1.0을 기록한 RFC 1945가 출판된 건 그로부터 2년 뒤인 1996년의 일이다. 쿠키는 이미 수많은 웹사이트에서 굴러가고 있었고, 표준은 그 뒤를 따라가고 있었다.

표준이 현실을 교정하려다 실패하다

IETF는 1997년 RFC 2109를 내놓으며 쿠키를 공식 표준으로 끌어올렸다. 제목부터 야심찼다. HTTP State Management Mechanism. 이 문서는 CookieSet-Cookie 헤더가 어떻게 여러 HTTP 요청을 하나의 stateful session으로 묶어내는지 설명했다. 여기서 말하는 세션은 TCP 연결을 계속 붙잡고 있는 게 아니라, 장바구니나 로그인처럼 이전 요청의 선택을 다음 요청에서 이어가는 논리적 맥락을 뜻였다.

하지만 여기서 멈추지 않았다. 2000년, IETF는 RFC 2965를 내놓으면서 Cookie2Set-Cookie2라는 새로운 헤더를 제안한다. 기존 쿠키보다 더 정교한 규격이었다. 이론적으로는 훌륭했다. 문제는 현실이었다. 브라우저도, 서버도 이 새 규격을 제대로 구현하지 않았다. 표준은 멀쩡했지만 아무도 쓰지 않았다. 상호운용성은 엉망이 됐고, 결국 Cookie2Set-Cookie2는 조용히 무덤으로 들어갔다.

현실이 표준이 되다

2011년, IETF는 전략을 완전히 바꾼다. RFC 6265의 접근법은 단순했다. "이미 인터넷에서 돌아가고 있는 CookieSet-Cookie의 실제 동작을 그대로 문서화하자." 더 이상 표준이 현실을 교정하려 하지 않았다. 현실을 그대로 받아들여 기록하는 쪽을 택한 거다. Cookie2Set-Cookie2는 공식적으로 폐기됐다. 표준이 현실을 교정하려다 실패하고, 다음 표준은 현실을 문서화한 셈이다. 원래 많은 일들이 그렇다 야심차게 표준을 정하지만, 보통 실패하고 현장에 있는 문화가 문서를 다시 재정의하는 것이다.

그리고 현재

RFC 6265는 아직 현역이다. 후속 문서인 RFC 6265bis는 이미 IESG 승인을 거쳐 RFC Editor로 넘어갔고, 이 글을 고친 2026년 8월 5일 현재 RFC-to-be 10025라는 번호를 받아 최종 출판을 기다리는 중이다. 그러니까 아직은 RFC 6265가 공식 폐기되지 않은 상태다. 새 문서가 출판되면, 거기에는 SameSite와 Cookie Prefixes 같은 현대 브라우저의 보안 동작이 정식으로 반영될 예정이다. 1994년 Netscape가 "일단 이렇게 하자" 하고 집어넣은 작은 기능이, 30년이 넘도록 진화하며 여전히 표준화 회의실의 의제로 살아 있다.

장바구니와 추적장치:쿠키의 어둠

몬툴리(Montulli)는 원래 목표에는 사이트를 넘나드는 공통 브라우저 식별자를 피하려는 의도가 있었다. 쿠키는 특정 서버와 그 서버를 방문하는 브라우저 사이에서만 오가는 값이어야 했다. 하지만 웹페이지는 자기 서버의 HTML만 받아오지 않는다. 이미지, 폰트, 광고 배너 같은 리소스는 전혀 다른 서버에서 가져온다.

HTML
<img src="https://ads.example/pixel.gif" alt="">

이 한 줄이 모든 걸 바꿨다. 사용자가 뉴스 사이트를 보든 쇼핑몰을 보든, 두 페이지에 같은 ads.example의 리소스가 박혀 있다면 브라우저는 그 요청마다 ads.example의 쿠키를 성실히 실어 보낸다. 뉴스 사이트와 쇼핑몰은 서로 아무 관련이 없지만, 광고 서버 입장에서는 그렇지 않다. 이 사용자는 방금 뉴스를 봤고, 지금은 쇼핑을 하고 있다. 브라우저는 아무 의심 없이 출석부를 제출하고 있었던 셈이다.

Montulli는 이 embedded content와 쿠키의 결합을 설계 당시 놓친 문제라고 회고했다. 1996년 무렵에는 제3자 쿠키를 통한 광고 추적이 논쟁이 됐다. 사용자를 기억하기 위해 만든 장치가 사용자의 이동 경로를 기억하는 데도 쓰이기 시작했다.

모두가 같은 방식으로 당하는 건 아니다

이 구조가 지금 모든 브라우저에서 같은 방식으로 통하는 것은 아니다. Safari는 2020년부터 제3자 쿠키를 기본으로 전면 차단했고, Firefox는 2022년부터 Total Cookie Protection을 기본으로 배포해 쿠키 저장소를 최상위 사이트별로 나눈다. 따라서 같은 ads.example이 여러 사이트에 들어가더라도, 이런 브라우저에서는 과거처럼 하나의 전역 쿠키만으로 방문을 곧바로 연결하기 어렵다.

거기에 더해서 Chrome은 2024년 7월 제3자 쿠키의 일괄 폐지 대신 사용자 선택을 높이는 방식으로 방향을 바꿨고, 2025년 4월에는 별도의 새 선택 창을 도입하지 않은 채 현재 접근을 유지한다고 발표했다. 같은 해 10월에는 Topics, Protected Audience, Attribution Reporting 등을 포함한 Privacy Sandbox 기술 대부분을 종료하되 CHIPS, FedCM, Private State Tokens는 유지한다고 정리했다.

요약하자면, 2026년의 제3자 쿠키 동작은 브라우저와 사용자 설정, 저장소 분할 여부에 따라 제각각이다. 위에서 설명한 추적 구조는 제3자 쿠키가 차단되거나 분할되지 않은 환경에서만 성립하는 이야기다.

당신은 상품이다

그리고 그 환경이 얼마나 위험한지는 공공 기관들의 조사가 잘 보여준다. 미국 FTC가 2014년에 조사한 데이터 브로커들은 파산 정보, 구매 기록, 웹 탐색 기록을 여러 출처에서 긁어모아 한 사람의 상세한 프로필로 합치고 있었다. 조사 대상 아홉 곳 중 일곱 곳은 다른 데이터 브로커와도 데이터를 주고받고 있었다.

영국 ICO의 실시간 광고 경매 조사더 소름 돋는 디테일을 보여준다. 웹사이트 방문 기록이나 건강 상태를 검색한 흔적까지 광고 입찰 정보에 포함될 수 있고, 단 한 번의 경매에서 개인 정보가 수백 개 조직에 노출될 수 있다고 지적했다. 이 모든 과정은 밀리초 단위로 끝난다. Google Authorized Buyers의 지표 문서가 밝힌 입찰 응답 제한 시간은 120~300ms이며, 개발자용 RTB 테스트 지침은 경매 형식에 따라 일반적으로 80~1000ms이고 정확한 제한은 BidRequest.tmax로 전달된다고 설명한다.

당신이 읽은 우울증 관련 기사, 검색한 파산 절차, 성인용품 쇼핑몰에서 보낸 시간은 이제 하나의 거대한 식별 프로필로 합쳐진다. 광고 한 칸이 경매되는 수백 밀리초 동안 그 프로필은 수백 개 조직 앞에 펼쳐진다. 당신은 더 이상 웹을 탐색하는 주체가 아니라, 완벽하게 정량화되어 실시간 입찰에 투입되는 데이터 상품이다.

하지만 적어도, 페이지를 넘겼을 때 장바구니에 담아둔 키보드가 사라지는 끔찍한 비극은 피할 수 있게 되었다.

물론 나는 오늘도 스판덱스 퍼리 슈트나, 말 못할 취미를 거리낌 없이 검색하며 아시아 30대 남성의 평균 데이터를 정성껏 망치고 있다. 내가 만들어내는 검색 기록은 광고주 입장에서 노이즈일까, 새로운 타겟 세그먼트일까. 어느 쪽이든 상관없다. 완벽한 프로파일링 따위, 내 검색 이력 앞에서는 그저 통계적 골칫거리일 뿐이다. 데이터 오염도 저항의 한 방식이라면, 나는 오늘도 꽤나 치열하게 싸우고 있는 셈이다.

쿠키를 안전하게 굽는 법

지금까지는 쿠키를 먹는... 아니지, 받는 입장에서 이야기했다면, 이제는 쿠키를 굽는 입장을 이야기할 차례다.
안타깝게도 프로그래머라는 직업은 종종 자신이 먹지도 못할 쿠키를 구워서 브라우저라는 손님에게 내밀어야 하는 숙명을 타고났다. 게다가 이 쿠키, 잘못 구우면 세션 탈취라는 식중독을 일으킨다. 그러니 제대로 굽는 법을 알아두자.

초기 쿠키 규칙은 단순했다. 밀가루 반죽에 초콜릿 칩 넣듯이 이름=값만 넣으면 끝이었다. 그런데 쿠키가 로그인 세션과 사용자 추적에 쓰이기 시작하면서, 언제 누구에게 이 쿠키를 내밀지 정하는 전송 조건이 점점 늘어났다. 이제는 쿠키 하나 구울 때도 따져야 할 게 한두 가지가 아니다.

먼저 기본 재료부터 구분하자.
Set-Cookie는 서버가 응답에 넣는 HTTP 헤더, 즉 "손님, 이 쿠키 받으세요" 하는 종업원의 멘트다.
session=opaque-id는 쿠키의 이름과 값, 그러니까 "이건 '세션'이라는 이름의 쿠키고, 안에는 opaque-id라는 내용물이 들었습니다" 하는 설명이다.

그리고 그 뒤에 세미콜론으로 붙는 Secure, HttpOnly, SameSite, Path는 "이 쿠키는 이렇게 취급해주세요" 하는 주의사항이다.

이것들은 브라우저 콘솔에서 실행하는 자바스크립트 명령어가 아니라는 점을 꼭 기억해두자. 주방에서나 통하는 주문이 손님이 있는 홀에도 통하는 것은 아니다.

물론 속성 몇 개를 붙인다고 세션이 완전히 안전해지는 것은 아니며, 세션 식별자의 생성과 회전, 서버 쪽 폐기, XSS와 CSRF 방어는 별도 문제다. 하지만 그걸 다 서술하는 것은 이 글의 범위를 넘어서므로 서술 하지 않고, 여기서는 브라우저가 쿠키를 보관하고 전송하는 범위를 제한하는 기본 안전장치부터 살펴보려고 한다.

Secure, HttpOnly, SameSite

Secure는 보안 연결에서만 쿠키를 보내도록 한다. 쿠키 값에 서명을 붙이거나 내용을 암호화하는 속성은 아니다. 이름은 자신감이 있지만 하는 일은 배송 경로 지정이다.

HttpOnlydocument.cookie 같은 비 HTTP API에서 쿠키를 제외한다. 브라우저 스크립트가 세션 쿠키를 직접 읽기 어렵게 만들지만, 브라우저는 조건이 맞는 HTTP 요청에 쿠키를 계속 붙인다.

OWASP도 HttpOnly가 보호하는 것은 쿠키 값의 기밀성이라고 선을 긋는다. XSS로 같은 출처에서 스크립트를 실행할 수 있는 공격자는 세션 ID를 읽지 못해도 이미 로그인된 브라우저로 요청을 보낼 수 있다. HttpOnly를 켜면 공격자가 세션 쿠키를 훔쳐가는 경로 하나는 닫히니 다행스러운 일이고, 굳이 훔치지 않은 채 그 자리에서 쓰는 경로만 남는다.

SameSite는 다른 사이트에서 시작된 요청에 쿠키를 보낼 조건을 제한한다. CSRF 위험을 줄이는 방어층이지만 서버의 CSRF token이나 요청 검증을 자동으로 대신하지는 않는다.

오늘날 유행하는 레시피

세 속성은 각자 하는 일이 다르지만, 로그인 세션 쿠키라는 음에서는 보통 함께 들어간다. 실제로 서버가 내놓는 한 줄짜리 레시피는 이렇게 생겼다.

Loading animated diagram...
HTTP
Set-Cookie: session=opaque-id; Secure; HttpOnly; SameSite=Lax; Path=/

이 한 줄을 재료별로 분해하면 이렇다.

부분

브라우저가 읽는 뜻

주방의 비유

Set-Cookie:

서버가 응답으로 쿠키 저장을 요청한다.

"손님, 쿠키 받으세요!"

session=opaque-id

이름이 session이고 값이 opaque-id인 쿠키다. 실제 서비스에서는 opaque-id 자리에 예측하기 어려운 세션 식별자가 들어간다.

포장지에 적힌 쿠키 이름과 그 안에 든 내용물.

Secure

보안 연결에서만 이 쿠키를 서버로 보낸다.

"이 쿠키는 안전한 길로만 배달해주세요."

HttpOnly

페이지의 JavaScript가 document.cookie로 이 쿠키를 읽지 못하게 한다.

"손님, 포장지 뜯지 마세요. 통째로 다시 주시면 됩니다."

SameSite=Lax

다른 사이트에서 시작된 요청에는 쿠키 전송을 일부 제한한다.

"외부 가게에서 링크를 따라 정문으로 들어오는 손님에게는 줄 수 있지만, 외부의 배달원등을 통해서 숨어서 요청한 경우에는 주지 않습니다"

Path=/

현재 호스트의 모든 URL 경로에 쿠키를 보낼 수 있다.

"가게 전체 메뉴에서 이 쿠키 유효합니다."

부분

Set-Cookie:

브라우저가 읽는 뜻

서버가 응답으로 쿠키 저장을 요청한다.

주방의 비유

"손님, 쿠키 받으세요!"

부분

session=opaque-id

브라우저가 읽는 뜻

이름이 session이고 값이 opaque-id인 쿠키다. 실제 서비스에서는 opaque-id 자리에 예측하기 어려운 세션 식별자가 들어간다.

주방의 비유

포장지에 적힌 쿠키 이름과 그 안에 든 내용물.

부분

Secure

브라우저가 읽는 뜻

보안 연결에서만 이 쿠키를 서버로 보낸다.

주방의 비유

"이 쿠키는 안전한 길로만 배달해주세요."

부분

HttpOnly

브라우저가 읽는 뜻

페이지의 JavaScript가 document.cookie로 이 쿠키를 읽지 못하게 한다.

주방의 비유

"손님, 포장지 뜯지 마세요. 통째로 다시 주시면 됩니다."

부분

SameSite=Lax

브라우저가 읽는 뜻

다른 사이트에서 시작된 요청에는 쿠키 전송을 일부 제한한다.

주방의 비유

"외부 가게에서 링크를 따라 정문으로 들어오는 손님에게는 줄 수 있지만, 외부의 배달원등을 통해서 숨어서 요청한 경우에는 주지 않습니다"

부분

Path=/

브라우저가 읽는 뜻

현재 호스트의 모든 URL 경로에 쿠키를 보낼 수 있다.

주방의 비유

"가게 전체 메뉴에서 이 쿠키 유효합니다."

참고로 서버가 나중에 받는 헤더의 이름은 Set-Cookie가 아니라 Cookie다. 앞의 줄이 서버에서 브라우저로 가는 저장 요청이라면, 아래 줄은 브라우저가 조건에 맞는 다음 요청에 저장된 값을 돌려주는 모습이다.

HTTP
Cookie: session=opaque-id

쿠키 굽기는 여기까지다. 잘못 구우면 손님 정보가 새지만 어쩌겠는가, 초보 프로그래머가 쿠키를 굽기 위한 과정이라고 손님을 이해시키는 것이 중요하다. 원래 프로그래머는 실패 속에서 자란다.

Prefix는 한국어로 접두사, 즉 이름 맨 앞에 붙는 문자열이다. Cookie Prefixes는 새로운 명령어나 별도의 쿠키 속성이 아니며, session이라는 이름 앞에 __Secure- 또는 __Host-를 붙여 __Secure-session, __Host-session처럼 만드는 이름 규칙이다. 밑줄 두 개와 마지막 하이픈까지 이름의 일부다.

쿠키 이름 전체는 대소문자를 구분하므로 SIDsid는 서로 다른 쿠키다. 다만 RFC 6265bis의 브라우저 처리 규칙은 접두사를 대소문자와 무관하게 검사한다. __secure-session__Secure-session과는 다른 이름이지만 Secure 조건을 피할 수는 없다. 서버와 프레임워크 사이의 혼동을 줄이려면 문서에 적힌 표준 형태인 __Secure-__Host-를 그대로 쓰는 편이 낫다.

보통 쿠키 이름은 애플리케이션만 해석하지만 이 두 접두사는 브라우저도 알아본다. 이름에 적힌 조건과 실제 속성이 맞지 않으면 브라우저는 쿠키를 저장하지 않는다. 개발자가 Secure를 빼먹거나 Domain, Path를 잘못 지정했을 때 이름과 설정의 모순을 브라우저가 발견하는 셈이다. Cookie Prefixes2016년 별도 Internet-Draft로 제안된 뒤 RFC 6265bis에 편입됐다.

이 규칙이 필요한 이유는 같은 이름의 쿠키라도 DomainPath에 따라 적용 범위가 달라질 수 있기 때문이다. Domain을 생략하면 쿠키는 그것을 설정한 호스트에만 묶이고, Domain=example.com처럼 지정하면 하위 도메인까지 범위를 넓힐 수 있다. Path=/account/account 아래의 요청에만 보내라는 뜻이고 Path=/는 호스트의 모든 경로를 뜻한다. 이름은 같은데 범위가 다른 쿠키가 함께 존재하면 서버가 예상하지 못한 값을 받을 수 있다.

__Secure-sessionsession이라는 쿠키 이름 앞에 __Secure-를 붙인 것이다. 이렇게 이름을 지어놓고 Secure 속성을 빼면 브라우저가 저장을 거부한다.

HTTP
Set-Cookie: __Secure-session=opaque-id; Secure; HttpOnly; SameSite=Lax; Path=/app

__Host-는 조건이 더 많다. SecurePath=/가 필요하고 Domain을 지정하면 안 된다. Domain이 없으니 쿠키는 설정한 호스트에만 묶이고, Path=/이므로 그 호스트의 일부 경로가 아니라 전체에 적용된다. 로그인 세션처럼 한 호스트가 소유해야 하는 값에 쓰기 좋은 이유다.

HTTP
Set-Cookie: __Host-session=opaque-id; Secure; HttpOnly; SameSite=Lax; Path=/

다음 조합은 브라우저가 거부해야 한다.

HTTP
Set-Cookie: __Secure-session=opaque-id; Path=/
Set-Cookie: __Host-session=opaque-id; Secure; Domain=example.com; Path=/
Set-Cookie: __Host-session=opaque-id; Secure; Path=/account
Set-Cookie: __Host-session=opaque-id; Path=/

각 줄이 거부되는 이유는 다음과 같다.

서버가 보낸 설정

위반한 Prefix 계약

브라우저 판정

__Secure-session=opaque-id; Path=/

__Secure-에는 반드시 있어야 하는 Secure가 없다.

거부

__Host-session=opaque-id; Secure; Domain=example.com; Path=/

__Host-Domain을 지정하면 안 된다. 현재 호스트에만 묶여야 하는데 하위 도메인으로 범위를 넓혔다.

거부

__Host-session=opaque-id; Secure; Path=/account

__Host-Path는 반드시 /이어야 한다. /account로 좁히면 호스트 전체를 대표한다는 계약이 깨진다.

거부

__Host-session=opaque-id; Path=/

Domain이 없고 Path=/인 점은 맞지만 Secure가 없다. __Host-에는 세 조건이 모두 필요하다.

거부

서버가 보낸 설정

__Secure-session=opaque-id; Path=/

위반한 Prefix 계약

__Secure-에는 반드시 있어야 하는 Secure가 없다.

브라우저 판정

거부

서버가 보낸 설정

__Host-session=opaque-id; Secure; Domain=example.com; Path=/

위반한 Prefix 계약

__Host-Domain을 지정하면 안 된다. 현재 호스트에만 묶여야 하는데 하위 도메인으로 범위를 넓혔다.

브라우저 판정

거부

서버가 보낸 설정

__Host-session=opaque-id; Secure; Path=/account

위반한 Prefix 계약

__Host-Path는 반드시 /이어야 한다. /account로 좁히면 호스트 전체를 대표한다는 계약이 깨진다.

브라우저 판정

거부

서버가 보낸 설정

__Host-session=opaque-id; Path=/

위반한 Prefix 계약

Domain이 없고 Path=/인 점은 맞지만 Secure가 없다. __Host-에는 세 조건이 모두 필요하다.

브라우저 판정

거부

브라우저는 빠진 속성을 대신 붙여주거나 잘못된 범위를 고쳐주지 않는다. 이름으로 선언한 계약과 실제 속성이 다르면 해당 쿠키 전체를 저장하지 않는다.

실제 브라우저에서 쿠키 보기

홈페이지에 접속한 브라우저에서도 쿠키를 직접 볼 수 있다. Chrome이나 Chromium 계열 브라우저에서 페이지를 연 뒤 F12를 눌러 DevTools를 열고, 상단에 Application이 보이지 않으면 » 메뉴에서 찾는다.
왼쪽의 Storage > Cookies를 펼쳐 현재 사이트를 누르면 브라우저가 저장한 쿠키가 나오며, Name, Domain, Path, HttpOnly, Secure, SameSite를 한 줄에서 같이 볼 수 있다. Chrome의 쿠키 저장소 확인 문서도 이 경로를 사용한다.

다만 Application에는 브라우저가 받아들인 쿠키만 남는다. 서버가 잘못된 Prefix 조합을 보냈다가 거부당했다면 이 목록에는 처음부터 나타나지 않으므로, 서버가 무엇을 보내려 했는지는 Network에서 따로 봐야 한다.

(한국어로 여자친구 사귀는 법 구글 검색 결과의 쿠키,
구글도 불가능한 것에 대한 해결책을 제시해주지 않는다.)

확인 순서는 다음과 같다.

  1. F12 > Network를 연 상태에서 페이지를 새로 고친다.

  2. 요청 목록에서 맨 위의 문서 요청을 고르고 Headers > Response HeadersSet-Cookie를 찾는다. 이것은 서버가 브라우저에 쿠키를 저장하라고 보낸 값이다. 이 헤더가 없다면 해당 응답에서는 새 쿠키를 설정하지 않은 것이다.

  3. 같은 요청의 Cookies 탭을 연다. 브라우저가 거부한 응답 쿠키에는 경고가 붙으며, Chrome은 More filters > Blocked response cookies로 이런 요청만 남기는 필터도 제공한다.

  4. 다음 요청의 Cookies > Request Cookies 또는 Headers > Request Headers에 있는 Cookie를 본다. 여기에 나타난 값이 브라우저가 서버로 실제로 돌려보낸 쿠키다. Chrome의 Network 쿠키 확인 절차는 요청을 선택한 뒤 Cookies 탭을 여는 방식이다.

  5. 마지막으로 Application > Storage > Cookies에서 현재 사이트를 선택해 저장 상태를 확인한다.

정리하면 Network의 응답 헤더에 보이는 Set-Cookie는 서버의 저장 요청이고, Application은 브라우저가 실제로 보관한 결과이며, 다음 요청의 Cookie 헤더는 브라우저가 서버로 돌려보낸 값이다. Prefix 조건이 틀리면 첫 단계에는 보이지만 뒤의 두 단계에는 나타나지 않는다.

Console의 document.cookie는 JavaScript에서 접근할 수 있는 쿠키를 읽는 브라우저 API다. HttpOnly 쿠키는 의도적으로 여기에서 숨겨지므로, Console만 보면 정상적으로 저장된 로그인 쿠키까지 없는 것처럼 보일 수 있다. 모든 홈페이지가 __Secure-__Host-를 쓰는 것도 아니어서 실제 목록에는 그 사이트가 발행한 쿠키만 나타나며, 접두사가 없는 쿠키라고 해서 자동으로 잘못된 쿠키가 되는 것은 아니다.

로그인 세션이 현재 호스트 하나에만 속한다면 __Host-를 먼저 고려한다. Domain을 사용해 여러 하위 도메인에 보내거나 /app처럼 더 좁은 Path가 꼭 필요하면 __Secure-를 쓸 수 있다. 이때 하위 도메인 전체가 같은 민감한 쿠키를 받도록 넓히는 판단은 별도로 검토해야 한다.

접두사는 쿠키 값에 서명하지 않고 탈취된 쿠키의 재사용도 막지 않는다. 브라우저가 잘못된 속성 조합을 저장하지 않게 할 뿐이다. 그래도 이름을 제대로 지었더니 브라우저가 서버의 실수를 거부한다. 컴퓨터 과학에서 작명이 실제 보안 기능이 되는 드문 경우다.

지금도 HTTP는 사용자를 기억하지 않는다

쿠키를 쓰더라도 HTTP 요청의 의미가 이전 연결에 자동으로 종속되는 것은 아니다. 매 요청마다 브라우저가 값을 다시 보내고, 서버가 그 값을 해석해 세션을 찾는다. 값이 없거나 만료됐거나 서버 저장소에서 폐기됐다면 사용자는 다시 로그인해야 한다.

쿠키가 한 일은 단순하다.

Loading animated diagram...
Text
서버가 작은 값을 보냄
브라우저가 조건에 맞춰 보관함
다음 요청에 그 값을 돌려보냄
서버가 애플리케이션 상태와 연결함

장바구니를 기억하는 쿠키 자체가 곧 동의 배너의 원인은 아니다. 인간은 좋은 기술이 있으면 그걸 어떻게든 나쁘게 만들기 위해 노력하는 족속이기때문이다.

영국 ICO 지침만 봐도 사용자가 고른 상품을 장바구니와 결제 과정에서 기억하는 쿠키는 요청한 서비스를 제공하는 데 꼭 필요한 경우 동의 예외가 적용될 수 있는 대표 사례다. 오늘날의 거대한 배너는 대체로 광고와 분석처럼 서비스 제공에 꼭 필요하지 않은 추적 기술까지 사용하려는 데서 나온다.

(저품질 광고중 그나마 건전한 광고)

물론 여기에는 숨은 딜레마가 있다. 정보를 하나도 안 주면 내가 보는 광고는 온통 "동네 싱글맘은 당신이 필요하다" 류의 저품질 배너로 채워진다.
자신의 동네에 싱글맘 숫자를 체크하고 싶다면 개인 정보를 주지 않아도 되지만, 정보를 조금이라도 넘긴다면 적어도 내게 필요한 물건을, 어쩌면 진짜 관심 있는 취미 용품을 보여줄 가능성이 생긴다.
어쩔 수 없이 광고를 볼 수밖에 없는 시대에 더 나은 광고 경험을 원한다면 더 많은 나를 내줘야 한다. 덜 노출되고 싶다면 더 저급한 광고를 감내해야 한다. 프라이버시 보호와 개인화 편의는 자주 충돌한다.
어느 한쪽을 당기면 다른 쪽이 딸려온다. 개인정보의 역설이다. 동의 배너 하나에도 이런 무역협상 같은 긴장이 숨어 있다.

물론 법은 사용자에게 실질적인 선택권을 주라고 요구하지만, 업계의 응답은 기가 막혔다. "모두 수락" 버튼은 화면 중앙에 크고 예쁘게 만들어놓고, 거부 선택지는 문단 속에 숨은 링크나 다음 화면으로 보내는 방식으로 처리했다. 이건 내가 지어낸 것이 아니고 실제로 EDPB의 Cookie Banner Taskforce 보고서는 실제로 이렇게 분류한다.

거부 버튼의 부재, 미리 선택된 체크박스, 눈에 띄지 않는 거부 링크, 기만적인 색상 대비—, 이것들은 다크 패턴으로 분류되었고, 이 모든 것이 실제 단속 사례 유형으로 등록되어 있다. 업계는 법을 지키는 척하면서 정작 법이 원한 선택권은 교묘하게 우회하는 데 심혈을 기울였다.

1994년에는 페이지를 넘기면 장바구니가 사라졌고, 2026년에는 쿠키 동의 창이 페이지보다 먼저 나타나는데, 사용자는 아직 본문에 도착하지 못했을 뿐, 장바구니는 남았으며, 상태 관리는 성공적이라고 할 수 있다.

이제 우리가 풀어야 할 문제는 "어떻게 상태를 유지할 것인가"가 아니다.
그건 30년 전에 선배들이 해결한 문제다. 지금 우리 앞에 놓인 것은 "어떻게 하면 쿠키 동의 창을 닫기 전에 진짜 본문을 읽을 수 있을 것인가"다.
장바구니는 지켜냈지만, 그 대가로 웹페이지 하나 여는 데 버튼 클릭이 두 번 필요한 시대가 되었다.
하나는 쿠키를 거부하는 클릭,
하나는 그걸 진심으로 받아들였는지 확인하는 클릭.
인류는 장바구니를 구했고, 그 댓가로 웹 서핑은 관료제가 되었다.

늘 그렇듯 훌륭하게 성공한 시스템은 완벽한 관료제로 진화하며, 우리는 그저 HTTP 헤더 하나를 굽기 위해 오늘도 끝없는 면책 조항에 동의할 뿐이다.

참고 자료

  1. Lou Montulli, The reasoning behind Web Cookies (2013): 1994년 Netscape의 장바구니 문제, 쿠키 설계, magic cookie라는 이름과 제3자 쿠키에 대한 회고다.

  2. Netscape Communications, Persistent Client State: HTTP Cookies: Netscape의 초기 비공식 쿠키 명세 보존본이다. 보존된 문서 자체에는 날짜가 없다.

  3. RFC 1945, Hypertext Transfer Protocol: HTTP/1.0 (1996): HTTP/1.0을 generic하고 stateless한 프로토콜로 설명한다.

  4. RFC 9110, HTTP Semantics (2022): 현재 HTTP 의미론에서 stateless를 요청 메시지의 의미가 독립적으로 해석되는 성질로 설명한다.

  5. RFC 2109, HTTP State Management Mechanism (1997): Netscape 제안을 바탕으로 CookieSet-Cookie를 표준화하려 한 첫 Standards Track RFC다.

  6. RFC 2964, Use of HTTP State Management (2000): URL 기반 상태 전달, 캐시, 개인정보 문제와 적절한 쿠키 사용을 다룬 BCP다.

  7. RFC 2965, HTTP State Management Mechanism (2000): Cookie2Set-Cookie2를 제안하고 RFC 2109를 대체했던 규격이다. 현재는 Historic이다.

  8. RFC 6265, HTTP State Management Mechanism (2011): 실제 배포된 쿠키 동작을 기준으로 상호운용 규칙을 다시 정리했다.

  9. RFC Editor, RFC-to-be 10025 final review, IETF Datatracker, draft-ietf-httpbis-rfc6265bis: RFC 6265의 후속 문서다. 2026년 8월 5일 현재 최종 검토 중이며 아직 출판된 RFC는 아니다.

  10. Mike West, Cookie Prefixes (2016 Internet-Draft): __Secure-__Host-를 별도 문서로 제안한 초안이다. 내용은 RFC 6265bis에 편입됐다.

  11. David M. Kristol, HTTP Cookies: Standards, Privacy, and Politics (2001): Netscape의 구현이 IETF 표준으로 바뀐 과정과 당시의 개인정보 논쟁을 기록했다.

  12. UNIX Programmer's Manual, Seventh Edition, fseek(3) (1979): magic cookie가 등장하는 이른 문서화 사례다.

  13. Lou Montulli, Why the name cookies? (2022 AMA): 운영체제 매뉴얼과 fortune cookie의 이미지가 이름에 영향을 줬다는 본인의 답변이다.

  14. Federal Trade Commission, Data Brokers: A Call for Transparency and Accountability (2014): 데이터 브로커의 정보 수집, 결합, 상호 거래를 조사한 보고서다.

  15. Information Commissioner's Office, Update report into adtech and real time bidding (2019): 실시간 광고 경매에서 개인 정보가 밀리초 단위로 여러 조직에 전달되는 구조와 민감 정보 문제를 다룬다.

  16. Google Authorized Buyers, View bidding metrics with RTB graphs: 공개된 실시간 입찰 응답 제한 시간이 120~300ms라고 설명한다.

  17. Roy T. Fielding, Architectural Styles and the Design of Network-based Software Architectures, 5.1.3 Stateless (2000): REST의 stateless 제약이 가시성, 신뢰성, 확장성에 주는 효과를 설명한다.

  18. OWASP, Session Management Cheat Sheet: URL 기반 세션 ID의 노출 경로와 HttpOnly가 막는 범위를 정리한다.

  19. Information Commissioner's Office, Cookies and similar technologies: 장바구니처럼 요청된 서비스에 꼭 필요한 쿠키의 동의 예외를 설명한다.

  20. European Data Protection Board, Report of the work undertaken by the Cookie Banner Taskforce (2023): 거부 버튼 부재, 미리 선택된 상자, 기만적 링크와 버튼 색상 등 실제 쿠키 배너 설계 문제를 분류한다.

  21. WebKit, Full Third-Party Cookie Blocking and More (2020): Safari가 교차 사이트 리소스의 쿠키를 기본으로 전면 차단한다고 발표한 글이다.

  22. Mozilla, Firefox rolls out Total Cookie Protection by default to all users worldwide (2022): Firefox가 쿠키를 최상위 사이트별 저장소로 분할하는 정책을 기본 배포한 기록이다.

  23. Google, A new path for Privacy Sandbox on the web (2024): Chrome의 제3자 쿠키 일괄 폐지 계획을 사용자 선택 중심으로 바꾼 발표다.

  24. Google, Next steps for Privacy Sandbox and tracking protections in Chrome (2025): 별도의 새 제3자 쿠키 선택 창을 도입하지 않고 현재 접근을 유지한다고 밝힌 발표다.

  25. Google, Update on Plans for Privacy Sandbox Technologies (2025): Privacy Sandbox 기술 대부분의 종료와 CHIPS, FedCM, Private State Tokens의 유지를 정리한 발표다.

  26. Google Authorized Buyers, Test your integration: 실시간 입찰의 일반적인 지연 제한 범위와 BidRequest.tmax를 설명한다.

각주

  1. https://cheatsheetseries.owasp.org/cheatsheets/SessionManagementCheat_Sheet.html#session-id-content-or-value
  2. https://www.rfc-editor.org/rfc/rfc2964.html#section-2
  3. https://www.unix.com/man_page/v7/3s/fseek/
  4. https://www.catb.org/jargon/html/M/magic-cookie.html
쿠키는 어떻게 웹의 기억이 되었을까