HTTP는 기본적으로 Stateless(무상태) 프로토콜이다.
즉, 서버는 사용자의 이전 요청을 기억하지 않는다.
예를 들어 사용자가 로그인에 성공했다고 해보자.
다음 요청이 들어왔을 때, 서버는 기본적으로 다음과 같은 정보를 알 수 없다.
왜냐하면 각각의 HTTP 요청은 독립적으로 처리되기 때문이다.
이 문제를 해결하기 위해 브라우저와 서버는 사용자 상태를 유지할 수 있는 방법이 필요했다.
그 방법 중 하나가 바로 쿠키(Cookie) 다.
쿠키는 서버가 브라우저에 저장하도록 지시하는 작은 데이터 조각이다.
서버가 응답을 보낼 때 Set-Cookie 헤더를 통해 쿠키 저장을 요청하면, 브라우저는 해당 값을 저장한다.
그리고 이후 동일한 사이트에 요청을 보낼 때 저장된 쿠키를 Cookie 헤더에 자동으로 포함해서 서버로 전송한다.
즉 쿠키는 단순한 브라우저 저장소가 아니라, 브라우저와 서버가 사용자의 상태를 이어가기 위해 사용하는 HTTP 기반 메커니즘이라고 볼 수 있다.
쿠키를 이해하려면 먼저 브라우저가 쿠키를 어떻게 저장하고, 어떤 기준으로 서버에 전송하는지 알아야 한다.
쿠키는 서버가 응답(Response)를 보낼 때 Set-Cookie 헤더를 통해 브라우저에게 저장을 요청하면서 시작된다.
Set-Cookie: sessionId=abc123
브라우저는 이 값을 저장한 뒤, 이후 요청에서 필요하다면 서버로 다시 전달한다.

쿠키의 가장 큰 특징은 브라우저가 자동으로 서버에 전송해준다는 점이다.
하지만 저장된 쿠키를 모든 요청에 무조건 첨부하는 것은 아니다.
브라우저는 요청을 보내기 전에 현재 요청 URL과 저장된 쿠키의 조건을 비교한 뒤, 전송 가능한 쿠키만 선택한다.
대략 다음 과정을 거친다.
Cookie 헤더에 자동으로 첨부한다.예를 들어 브라우저에 다음과 같은 쿠키가 저장되어 있다고 가정해보자.
Set-Cookie: sessionId=abc123
사용자가 아래 페이지로 이동하면
https://example.com/profile
https://example.com/orders
https://example.com/cart
브라우저는 자동으로 쿠키를 첨부한다.
Cookie: sessionId=abc123
덕분에 서버는 사용자가 로그인한 상태인지 매 요청마다 확인할 수 있다.
로그인 유지, 세션 관리, 사용자 설정 저장 등이 모두 이러한 동작을 기반으로 이루어진다.

브라우저는 쿠키를 저장할 때 단순히 값만 저장하지 않는다.
함께 전달된 여러 옵션도 함께 저장하며, 요청 시 이 조건들을 확인한다.
대표적으로 다음과 같은 속성이 있다.
이번 글에서는 Domain, Path, HttpOnly를 중심으로 살펴보자.

모든 쿠키가 모든 도메인에 전송되는 것은 아니다.
브라우저는 쿠키를 저장할 때 이 쿠키가 어떤 도메인에서 사용될 수 있는지도 함께 기록한다.
Set-Cookie: sessionId=abc123; Domain=.example.com
위와 같이 Domain=.example.com이 설정되어 있다면 다음과 같은 서브도메인에서도 쿠키를 사용할 수 있다.
api.example.com
www.example.com
admin.example.com
반면 Domain을 명시하지 않으면 기본적으로 쿠키를 설정한 현재 호스트에만 적용된다.
Set-Cookie: sessionId=abc123
즉 Domain은 브라우저가 “이 쿠키를 어떤 도메인 요청에 포함할 수 있는가?”를 판단하는 기준이 된다.
특정 경로에서만 쿠키를 전송하도록 제한할 수 있다.
Set-Cookie: sessionId=abc123; Path=/api
그러면 다음 요청에서는 쿠키가 전송된다.
/api/user ⭕
/api/books ⭕
하지만 아래 요청에서는 전송되지 않는다.
/products ❌
브라우저는 요청 URL의 경로를 확인한 뒤, Path 조건을 만족하는 경우에만 쿠키를 Cookie 헤더에 첨부한다.
즉 Path는
"이 쿠키를 어떤 URL 경로에서 사용할 수 있는가?"
를 결정하는 기준이다.

지금까지 살펴본 Domain과 Path는
"쿠키를 언제 서버로 전송할 것인가?"
에 대한 조건이었다.
반면 HttpOnly는 조금 다른 역할을 한다.
HttpOnly는 쿠키를 JavaScript로부터 보호하기 위한 옵션이다.
Set-Cookie: refreshToken=abc123; HttpOnly
HttpOnly가 설정된 쿠키는 JavaScript에서 접근할 수 없다.
document.cookie
로 조회하더라도 해당 쿠키는 보이지 않는다.
하지만 중요한 점은, 브라우저의 자동 전송에는 영향을 주지 않는다는 것이다.
JavaScript 접근 ❌
브라우저 자동 전송 ⭕
즉 브라우저는 여전히 조건을 만족하는 요청에 해당 쿠키를 자동으로 포함한다.
이 때문에 Refresh Token과 같이 노출되면 위험한 값은 일반적으로 HttpOnly Cookie에 저장한다.
XSS 공격이 발생하더라도 악성 스크립트가 토큰 값을 직접 읽어가는 위험을 줄일 수 있기 때문이다.
정리하면 브라우저는 다음과 같은 방식으로 쿠키를 관리한다.
즉 쿠키의 저장과 전송은 개발자가 직접 처리하는 것이 아니라 브라우저가 담당한다.
이제 이러한 브라우저의 동작을 이해했으니, Next.js에서는 쿠키를 어떻게 읽고 설정하는지 살펴보자.
지금까지 브라우저가 쿠키를 저장하고 서버에 자동으로 전송하는 과정을 살펴보았다.
그렇다면 브라우저가 전송한 쿠키를 Next.js는 어디에서 읽을 수 있을까?
Next.js에서는 컴포넌트가 실행되는 위치에 따라 쿠키를 읽는 방법이 달라진다.
Client Component는 브라우저에서 실행된다.
따라서 브라우저 API인 document.cookie 를 사용할 수 있다.

브라우저에 다음 쿠키가 저장되어 있다고 가정해보자.
sessionId=abc123 theme=dark
그러면 JavaScript에서 document.cookie를 조회하면 다음과 같은 문자열을 얻을 수 있다.
sessionId=abc123; theme=dark
물론 httpOnly 설정이 되어 있는 쿠키는 해당 값을 확인 할 수 없다.
반면 Server Component는 브라우저가 아닌 서버에서 실행된다.
따라서 document.cookie를 사용할 수 없다.
대신 Next.js가 제공하는 cookies()API를 사용해야 한다.

그런데 여기서 한 가지 궁금증이 생긴다.
Client Component의 document.cookie는 브라우저에 저장된 쿠키를 직접 읽는다는 것을 이해했다.
그렇다면 Server Component에서 사용하는 cookies()는 어디에서 값을 가져오는 걸까?
많은 사람들이 cookies()도 브라우저 저장소를 읽는 API라고 생각하지만 실제로는 조금 다르다.
Server Component는 브라우저에 접근할 수 없기 때문에 브라우저 저장소를 직접 읽을 수 없다.
대신 브라우저가 요청과 함께 전송한 Cookie 헤더를 읽는다.
GET /mypage
Cookie: sessionId=abc123
그리고 Next.js 서버는 이 요청 헤더를 받는다.
즉 흐름은 다음과 같다.

즉 cookies()는 브라우저 저장소를 읽는 API가 아니다.
브라우저가 요청과 함께 전송한 Cookie 헤더를
Next.js가 읽기 쉽게 제공하는 API라고 이해하면 된다.
Server Component에서는 cookies()를 통해 요청에 포함된 쿠키를 읽을 수 있었다.
그렇다면 Middleware에서는 어떻게 쿠키를 읽을까?
Middleware 역시 현재 요청(Request)을 전달받기 때문에
브라우저가 전송한 Cookie 헤더를 읽을 수 있다.
다만 Server Component와 가장 큰 차이점은
Middleware가 페이지 렌더링보다 먼저 실행된다는 점이다.
사용자가 /mypage에 접근하면 요청은 먼저 Middleware를 통과한 뒤 페이지에 도달한다.


Middleware는 요청이 페이지에 도달하기 전에 실행되므로,
쿠키를 기반으로 인증 여부를 확인하거나 리다이렉트를 수행하기에 적합하다.
Next.js에서는 Route Handler에서 응답을 만들 때 쿠키를 설정할 수 있다.
예를 들어 로그인 API를 만든다고 가정해보자.

이 코드는 응답에 Set-Cookie 헤더를 포함시킨다.
HTTP/1.1 200 OK
Set-Cookie: sessionId=abc123; HttpOnly; Path=/; Max-Age=3600
브라우저는 이 응답을 받으면 sessionId=abc123 쿠키를 저장한다.
즉 Route Handler에서 쿠키를 생성한다는 것은 다음 흐름을 의미한다.
Route Handler
↓
Response에 Set-Cookie 헤더 추가
↓
Browser가 응답 수신
↓
Browser가 쿠키 저장
next.js에서 제공하는 api인
cookies().set()으로도 쿠키를 설정할 수 있다

이 방식도 결과적으로는 동일하다.
cookies().set()을 호출하면 Next.js는 응답에 Set-Cookie 헤더를 포함시킨다.
Route Handler에서 쿠키를 생성할 때 중요한 점은, 서버가 브라우저의 쿠키 저장소를 직접 수정하는 것이 아니라는 점이다.
서버는 단지 응답에 Set-Cookie 헤더를 담아 보낸다.
그리고 브라우저가 그 응답을 보고 쿠키를 저장한다.
정리하면 쿠키를 읽을 때는 Request의 Cookie 헤더를 읽고, 쿠키를 생성할 때는 Response의 Set-Cookie 헤더를 만든다.
지금까지 쿠키를 읽는 여러 방법을 살펴보았다.
하지만 중요한 점은 쿠키가 서버에 저장되는 데이터가 아니라 브라우저가 관리하는 데이터라는 점이다.
서버는 브라우저의 쿠키 저장소에 직접 접근 할 수 없다.
대신 HTTP 헤더를 통해 브라우저와 약속된 방식으로 통신한다.
결국 서버는 쿠키를 직접 조작하는 것이 아니라,
브라우저에게 쿠키를 읽어달라고 요청하거나 저장해달라고 요청하는 것이다.
번외)
Next.js에서 cookies()를 사용할 때 한 가지 중요한 특징이 있다.
바로 cookies()를 사용하는 페이지는 Dynamic Rendering 대상이 된다는 점이다.
처음에는 조금 의아할 수 있다.

위 코드는 단순히 쿠키 하나를 읽는 것처럼 보인다.
그런데 왜 이 코드가 정적인 페이지가 아니라 동적인 페이지로 처리될까?
먼저 Static Rendering을 생각해보자.
Static Rendering은 말 그대로 HTML을 미리 만들어두는 방식이다.
예를 들어 모든 사용자에게 동일한 페이지를 보여주는 경우라면 빌드 시점에 HTML을 만들어두고 재사용할 수 있다.
export default function Page() {
return <div>About Page</div>;
}
이 페이지는 사용자마다 달라질 정보가 없다.
누가 요청하더라도 결과는 항상 같다.
User A → About Page
User B → About Page
User C → About Page
이런 페이지는 미리 만들어두고 재사용해도 문제가 없다.
User A → Cookie: theme=dark
User B → Cookie: theme=light
User C → Cookie: theme=system
같은 /mypage 페이지를 요청하더라도 사용자의 쿠키 값에 따라 결과가 달라질 수 있다.

이 경우 User A에게는 다음 HTML이 필요하다.
<div>현재 테마: dark</div>
User B에게는 다른 HTML이 필요하다.
<div>현재 테마: light</div>
즉 Next.js 입장에서는 이 페이지를 미리 하나의 HTML로 만들어둘 수 없다.
요청이 들어와야만 어떤 쿠키가 포함되어 있는지 알 수 있기 때문이다.
cookies()는 현재 요청에 의존한다앞에서 살펴본 것처럼 cookies()는 브라우저 저장소를 직접 읽는 API가 아니다.
현재 요청에 포함된 Cookie 헤더를 읽는다.
GET /mypage
Cookie: theme=dark
즉 cookies()는 다음과 같은 요청 시점의 정보에 의존한다.
Request
└─ Cookie Header
└─ theme=dark
요청이 들어오기 전에는 쿠키 값을 알 수 없다.
그래서 Next.js는 cookies()를 사용하는 페이지를 정적으로 미리 만들 수 없다고 판단한다.
이때 페이지는 Dynamic Rendering으로 처리된다.
Dynamic Rendering은 요청이 들어올 때마다 서버에서 렌더링하는 방식이다.
User A Request
└─ Cookie: theme=dark
└─ render → 현재 테마: dark
User B Request
└─ Cookie: theme=light
└─ render → 현재 테마: light
즉 사용자마다 다른 쿠키 값을 반영할 수 있다.
정리하면 다음과 같다.
cookies()
↓
현재 Request의 Cookie Header를 읽음
↓
요청마다 값이 달라질 수 있음
↓
미리 HTML을 만들 수 없음
↓
Dynamic Rendering
물론 cookies()를 사용한다고 해서 무조건 나쁜 것은 아니다.
마이페이지처럼 사용자별 정보가 필요한 페이지라면 Dynamic Rendering이 자연스럽다.
다만 모든 페이지에서 습관적으로 cookies()를 사용하면 정적으로 처리할 수 있는 페이지까지 동적으로 렌더링될 수 있으므로, 쿠키가 꼭 필요한 위치에서만 읽는 것이 좋다.
지금까지 Next.js에서 쿠키를 읽고 생성하는 방법을 각각 살펴보았다.
정리하면 Next.js에서 쿠키를 다룬다는 것은 크게 세 가지 관점으로 나눌 수 있다.
아래 그림으로 전체 구조를 한 번에 정리해보자.

Route Handler에서는 응답에 Set-Cookie 헤더를 추가해 브라우저에게 쿠키 저장을 요청한다.
or
서버에서 Set-Cookie 헤더를 추가해 브라우저에게 쿠키 저장을 요청한다.
브라우저는 이 쿠키를 저장하고, 이후 요청마다 조건에 맞는 쿠키를 Cookie 헤더에 자동으로 포함한다.
Next.js는 이 요청에 포함된 쿠키를 실행 위치에 따라 다르게 읽는다.
Middleware에서는 request.cookies를 사용하고, Server Component에서는 cookies()를 사용한다.
그리고 cookies()는 현재 요청의 Cookie 헤더에 의존하기 때문에 사용자마다 결과가 달라질 수 있다.
이 때문에 cookies()를 사용하는 페이지는 정적으로 미리 만들어두기 어렵고, Dynamic Rendering으로 처리된다.
결국 Next.js에서 쿠키를 이해한다는 것은 브라우저 저장소를 직접 조작하는 방법을 아는 것이 아니라, Request와 Response의 헤더 흐름을 이해하는 것이다.