React-Django 회원가입을 구현하는 과정중에 겪었던 Session Cookie 발급 동작에 대해 정리해보려고 합니다.
Middleware를 통해 Session Cookie 발급이 어떻게 동작하는지 이해하고 있었다고 생각했는데 완전 착각이었습니다.
지연 로딩(Lazy Loading)에 대한 이해가 부족했습니다.
모쪼록 이번 회원가입 구현을 통해 다시한번 Session Cookie 발급 동작을 정리해보겠습니다.

요구사항에 따른 회원가입 구현에서는 도식화의 ④의 API에서 비밀번호 유효성 검증 후 ⑤의 API로 전달해야하는 과정이 있습니다.
request.session["password1"] = serializer.validated_data["password1"]
request.session["password2"] = serializer.validated_data["password2"]
⑤의 API로 비밀번호를 전달하기 위해 session에 검증된 비밀번호를 저장합니다.
테스트 해볼 겸 session에 저장한 검증된 비밀번호만 확인하기 위한 따로 API 생성하였고 값을 확인해보니 아무것도 저장되어있지 않았습니다.

session에 저장한 비밀번호가 확인 되지 않으니
우선적으로 admin의 세션테이블과 브라우저 개발자 도구를 확인하였습니다.
admin과 개발자 도구에서 동일하게 확인되는 session과 정체를 알수 없는 session을 확인하였습니다.
이후 동일하게 확인되는 session외 나머지 session은 ④의 API가 실행될 때마다 새롭게 생성 된 session임을 확인하였습니다.
하지만 여기까지 파악한 session 발급 동작은 이해하고 있던 것과는 달랐습니다.
① 서버에 첫 요청 시 middleware를 통해 요청 브라우저의 sessionid cookie를 확인
② sessionid cookie가 없다면 session key를 생성하여 요청 브라우저에 sessionid cookie를 응답으로 반환.
③ 이후 요청헤더에서 sessionid cookie를 통해 인증을 수행.
제 이해로는 브라우저는 서버에 첫 요청인 ①의 API와 그리고 ③의 API를 거쳐왔기 때문에 Session Cookie는 이미 생성된 상태이여야 합니다!
왜 새로운 session가 발급이되며 ④의 API에 저장된 비밀번호 값을 브라우저의 session로 확인할 수 없는지 이해가 되지 않았습니다.
방법을 찾던 중 CORS 설정에 의해 React-Django 간 통신이 허용되지 않아 생기는 문제일 가능성이 있다고하여 CORS와 관련된 설정을 확인하였습니다.
웹 애플리케이션의 출처는 도메인, 프로토콜, 포트 번호의 조합으로 정의됩니다.
본적으로 브라우저는 보안상의 이유로 다른 출처에서의 리소스 접근을 제한합니다.
이러한 제약을 해제하고 필요한 경우에 다른 출처의 리소스 접근을 허용하기 위해 CORS가 사용됩니다.
예를 들어 현재의 프로젝트의 도메인, 프로토콜, 포트번호를 확인해보면 다음과 같습니다.
HTTP
react : http://127.0.0.1:3000/
django : http://127.0.0.1:8000/
도메인, 프로토콜은 같지만 React-Django 간 포트번호는 다르기 때문에 현재 환경에서 CORS 설정이 필요합니다.

다른 강의에서 CORS설정이 필요해서 설정했던 적이 있습니다.
하지만 다시 확인해보니 Access-Control-Allow-Credentials 설정이 빠져있었습니다.⎝⍥⎠
이 경우에 필요한 CORS 설정을 정리하면 다음과 같습니다.
Access-Control-Allow-Origin: 어떤 출처에서 오는 요청을 허용할지 지정합니다. 와일드카드(*)를 사용하면 모든 출처에서의 요청을 허용할 수 있습니다.
Access-Control-Allow-Credentials: 클라이언트가 쿠키와 같은 자격 증명을 요청에 포함할 수 있도록 허용합니다.(중요설정!)
설정 이후 개발자 도구에서 요청에 대한 이 두값을 확인할 수 있습니다.

그리고 추가적으로 React 요청에 쿠키와 같은 자격증명을 포함할 수 있도록해야합니다.
export const validatePwd = async (urlCode, emailCode, password1, password2) => {
try {
const response = await axios.post(`http://localhost:8000/api/validate-pwd/${urlCode}/${emailCode}`,
{password1, password2},
{ withCredentials: true },
);
django API에 요청하는 axios.post에 withCredentials: true 옵션을 추가해줍니다.
다른 출처의 리소스 접근을 허용하기 위해서는 백엔드에서 CORS 설정이 필요합니다. 백엔드 뿐만아니라 프론트엔드 API에서도 withCredentials 옵션을 추가하여 요청에 쿠키와 같은 자격증명을 포함해야 이를 통해 백엔드에서 다른 출처에 대한 인증이 가능합니다!
④의 API 요청에서 이 CORS 설정이 없었기 때문에 Django에서는 해당 요청의 브라우저에 Session Cookie가 없다고 판단하여 ④의 API 요청 때마다 새롭게 Session Cookie를 발급한 것이 었습니다.
④의 API 요청마다 Session Cookie가 계속 발급되는 이유를 이해 하였습니다!
하지만 이때까지도 ④의 API 전 이미 서버의 첫 요청이 완료되어 Session Cookie가 존재하는데 계속 새롭게 Session Cookie가 발급되는건지 이해가 되지 않았습니다,,
여러 시도 중 기존 세션을 지워보는 시도에서 ④의 API 이전 API에서 Session Cookie가 새롭게 발급이 되지 않고 여전히 ④의 API에서만 새롭게 세션이 생성이 됨을 확인하였습니다!
이 시도에서 Session Cookie 발급 동작에 대한 이해가 잘 못되었음을 직감하고 자료를 다시 한번 확인하였습니다.
잘못 이해하고 있던 발급 동작은 아래의 내용입니다.
① 서버에 첫 요청 시 middleware를 통해 요청 브라우저의 sessionid cookie를 확인.
결론부터 이야기하자면 서버에 첫 요청시 request.session 객체가 생성되는 것이지 브라우저에 Session Cookie가 발급되는 것이 아니었습니다.
이는 지연 로딩(Lazy Loading)과 관련이 있습니다.
단순히 접속만한다고 Session Cookie가 브라우저에 전달되는 것이 아니었습니다.
class SessionMiddleware(MiddlewareMixin):
def __init__(self, get_response):
super().__init__(get_response)
engine = import_module(settings.SESSION_ENGINE)
self.SessionStore = engine.SessionStore
def process_request(self, request):
session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
request.session = self.SessionStore(session_key)
request.session 객체는 process_request 메서드에서 생성됩니다.
이 메서드는 view를 호출하기 전에 호출됩니다.
① request.COOKIES.get()으로 Session Cookie(sessionid)를 조회하여 Session_key를 생성합니다.
② Session_key로 SessionStore를 조회하고 없다면 새로운 sessionkey를 생성해서 request.session 객체를 생성합니다.
이 시점에서는 아직 클라이언트에게 session cookie를 전송하지 않습니다.
def process_response(self, request, response):
response.set_cookie(
settings.SESSION_COOKIE_NAME,
request.session.session_key,
max_age=max_age,
expires=expires,
domain=settings.SESSION_COOKIE_DOMAIN,
path=settings.SESSION_COOKIE_PATH,
secure=settings.SESSION_COOKIE_SECURE or None,
httponly=settings.SESSION_COOKIE_HTTPONLY or None,
samesite=settings.SESSION_COOKIE_SAMESITE,
)
return response
process_response는 view가 반환되고 나서 호출됩니다
① 세션이 비어있다면 세션을 삭제합니다.
② 세션이 변경된 경우 세션을 저장하고, 만료시간을 계산하여 Cookie(set_cookie)에 반영합니다.
Django가 session cookie를 전달하는 시점은 첫 요청(접속)이 아닌 세션이 변경된 경우인 것을 알 수 있습니다.
세션 데이터 저장은 API에서 request.session을 통해 세션 데이터를 설정하는 것입니다.
request.session["password1"] = serializer.validated_data["password1"]
request.session["password2"] = serializer.validated_data["password2"]
이 때, 세션 데이터가 설정되면 Django는 세션이 변경된 것으로 간주합니다.
세션이 변경된 경우, Django는 응답 시점에 세션 데이터를 저장하고 session cookie를 클라이언트로 전송할 준비를 합니다.
응답을 보낼 때, 클라이언트의 쿠키에 세션 키를 설정합니다.
이렇게 하면 클라이언트 브라우저는 세션 키를 쿠키로 저장하게 됩니다.
이후의 요청에서는 클라이언트가 이 세션 키를 서버로 전송하게 되어 동일한 세션 데이터를 사용할 수 있게 됩니다.(CORS설정필요!)
지연 로딩은 실제로 데이터가 필요할 때까지 데이터를 로드하지 않는 것입니다.
지연로딩(Lazy Loading)이 이루어지는 구간입니다.
request.session은 SessionStore 객체입니다.
request.session["password"] = password
데이터를 저장하여 세션 데이터를 변경한다면
def __setitem__(self, key, value):
self.session[key] = value # self.session은 _session_cache를 반환
self.modified = True # 세션 데이터가 변경되었음을 표시
@property
def session(self):
if self._session_cache is None: # 첫 접근 시 로드
self.load()
self.accessed = True
return self._session_cache
SessionStore 객체의 setitem 메서드가 실행됩니다
self.session[key] = value에서 self.session이 호출되면,
_session_cache가 None인지 확인합니다.
_session_cache가 None이면 load 메서드가 호출되어 데이터베이스에서 세션 데이터를 가져옵니다.
def load(self):
if self._session_cache is None: # _session_cache가 None일 때만 로드
try:
session_data = self._get_session(self.session_key) # 세션 저장소에서 데이터 가져오기
self._session_cache = self.decode(session_data) # 가져온 데이터를 디코딩하여 _session_cache에 저장
except KeyError:
self._session_cache = {} # 세션 키가 존재하지 않으면 빈 세션으로 초기화
return self._session_cache
왜 load 메서드가 호출되는가?
기존 세션 데이터를 유지하면서 새로운 데이터를 추가하거나 변경하기 위해서입니다.
_session_cache가 None이라는 것은 세션 데이터가 아직 메모리에 로드되지 않았음을 의미합니다.
세션 데이터를 설정하려면 기존 세션 데이터와 새로운 데이터를 함께 '메모리'에서 관리해야 합니다.
기존 세션 데이터가 없다면, _session_cache는 빈 딕셔너리로 초기화됩니다.
이후 self.session은 _session_cache를 반환하고, 세션 데이터는 _session_cache에 저장됩니다.
왜 지연 로딩인가?
세션 데이터에 처음 접근할 때(데이터를 읽거나 쓸 때) 데이터베이스에서 세션 데이터를 로드합니다. 이로써 불필요한 데이터 로드를 방지하고 성능을 최적화합니다.
이외 요청에 대해서는 데이터베이스 접근이 발생하지 않습니다.
request.session["password"]와 같이 세션 데이터를 설정할 때, _session_cache가 None이면 세션 데이터를 로드합니다. 이는 세션 데이터 변경 작업이 기존 데이터를 기반으로 이루어져야 하기 때문입니다.
세션 데이터 변경 작업이 있을 때만 로드가 발생하므로, 필요할 때만 데이터를 로드하는 지연 로딩이 적용됩니다.
정리하고나서보니 ④의 API는 정상적으로 기능하고 있었지만 session cookie 발급에 대한 이해가 부족해 생긴 문제였습니다.
정리를하자면 다음과 같습니다.