티스토리 뷰

오늘의 주인공 되시겠다

이 글은 아래 로그인 기능에 관한 개념 정리글과 이어지는 글이다. 만약 로그인 기능과 관련된 필수 지식들을 톺아보고 싶다면 아래 글을 읽고 이 글을 읽는 걸 추천한다.

[링크]

 

이 글은 프로젝트에서 눈물겨웠던 삽질기에 관한 내용이며, 때문에 두서가 없을 수도 있다. 그리고 무엇보다 결국 해결을 못한 슬픈 문제라 살짝 (아니 좀 많이) 속상하다🥺.. 해결을 못했더라도 기록을 하는 이유는 삽질을 하면서 배운 게 많기 때문이고, 다른 해결책도 찾았기 때문이다... 또로롱..

 

간단하게 우리 팀 프로젝트의 로그인 기능을 설명하자면

1. 유저가 로그인을 하게 되면 JWT 토큰 방식으로 Access Token과 Refresh Token을 넘겨준다.
2. 서버는 Access Token은 API response body로 넘겨주고, Refresh Token은 HTTP 응답 헤더 중 set-cookie 헤더에 담아 전달이 된다.
3. 클라이언트에서는 Access Token을 쿠키에 담아 저장을 해주고, Access Token이 만료될 경우 유효한 Refresh Token을 기반으로 Access Token 갱신 API를 호출한다. 
4. 로그아웃을 하면 Access Token과 Refresh Token을 삭제한다. 

 

Access Token의 경우 API 요청을 날리면 암호화된 상태로 response에 담겨 오기 때문에 클라이언트에서 작업하기 수월했다. Access Token을 받으면 그걸 setAccessToken이라는 함수를 통해 cookie에 저장하는 방식으로 처리해주면 끝났다.

import { Cookies } from 'react-cookie';

const cookies = new Cookies();

//setAccessToken 함수의 모습이다
export const setAccessToken = (accessToken: string): void => {
    const today = new Date();
    const expireDate = today.setDate(today.getDate() + 7);

    cookies.set('access_token', accessToken, {
        sameSite: 'strict',
        path: '/',
        expires: new Date(expireDate),
    });
};

하지만 문제는.. Refresh Token이였다.

 


🧨 문제의 서막

로그인 기능을 완료했다고 생각하고 로그인을 한 후 다른 기능들을 개발하고 있던 중이었다. 일정 시간이 지나면 콘솔에 계속 이런 에러가 떴다.

끄앙

토큰 갱신에 실패했다는 에러가 떴고, 그때마다 다시 로그인을 해줘야 했다. 하지만 처음에 이 문제를 발견했을 때는 일단 로그인이 되기는 하고(..), 개발해야하는 기능들이 많이 있었기 때문에 (로그인/회원가입 기능을 제일 먼저 개발했으니께!) 일단 노션에 고쳐야 할 에러로 기록만 해놓고 넘어갔다. 왜 그랬니 나야~!

 


😳 문제 원인 발견

얼추 기능 구현이 다 마무리되었을 때 오래 묵혀두었던 이 에러를 다시 꺼내보았다. 실제로 내가 맡은 부분에서 가장 마지막으로 수정을 봐야 할 친구가 이 에러였다. 자잘한 디자인 수정을 빼면!

 

정확한 문제의 원인을 찾아보기 위해 여러 가설을 세워봤었다🤔 생각이 나는 건 크게 아래 두 가지였다.

 

1) axiosInstanceInterceptor 의 문제인가?!

 

처음에는 내가 axiosInstanceInterceptor 설정을 잘 못 해줘서 에러가 난 줄 알았다. 이번 프로젝트에서 api 초기 코드 셋팅을 내가 맡았었는데, 이 때 api 모듈화를 적용해보려했다. 조금 더 간단하게 api 호출을 해보기 위해서였다. Api 모듈화를 시도하기 위해 공부를 하던 중 axiosInstanceInterceptor 이라는 개념을 알게 되었고, 코드도 한 번 써보았다. 해당 개념을 처음 알게 되었고 코드에 적용을 해보았던 부분이었기 때문에, 에러가 났다면 분명 여기서 발생했을 것이라는 생각을 했다. 

 

그래서 구글링을 통해 코드를 좀 더 구체적으로 짰다. 내 기억으로는 에러의 경우의 수를 좀 더 세분화해서 (ex.401에러일때, 404 에러일때...) 에러 메시지가 다르게 뜨게 하는 방식으로 코드를 수정했던 것 같다. 

//예상되는 당시 대략적인 코드 플로우. 수정을 넘 많이 했어서 코드를 못찾겠다...;-;
axiosInstance.interceptors.response.use(
    (response) => {
        if (response.status === 404) {
            console.log('404 에러 페이지로 넘어가야 함!');
        }

        return response;
    },
    async function (error) {
        const {
            config,
            response: { status },
        } = error;
        console.log('에러입니다', status);

        if (status === 401) {
           ...
        }

        return Promise.reject(error);
    },
);

에러 원인을 정확하게 찾기 위해 계속 콘솔을 찍어보았지만, 여기가 문제가 아니였다.

더보기

그리고 이때 이곳저곳 찔러보다가 Access Token의 유효기간이 30분으로 설정이 되어있다는 것을 발견하게 되었다 ㅎㅎ.. 서버한테 물어보니 자기가 넘겨줄 때 30분으로 설정을 했다고 한다. 즉, 나는 30분마다 로그인을 했어야 했던것!

expirationDate를 콘솔에 찍으면 몇년 몇월 며칠 몇시에 만료가 되는지 나왔다!

 

2) Refresh Token 자체의 문제일까?!

 

문제의 원인이 뭘까 이곳 저곳 찔러보던 중 '애초에 Refresh Token이 쿠키에 저장이 되지 않는 걸까..?' 라는 불안한 생각이 들었다.  원래 내가 생각했던 플로우는 API 요청 이후 Access Token과 Refresh Token이 함께 전달이 된다 ➡ 클라이언트에서 정의된 setRefreshTokensetAccessToken 함수로 각각의 토큰을 쿠키에 저장한다 ➡ 토큰들을 활용한다! 였고, 이대로 코드를 작성하였다. 하지만 함수를 이용해 제대로 토큰들을 저장을 했음에도 불구하고.. Refresh Token을 콘솔로 찍어보았을 때 undefined만이 리턴이 되었다. 

 

결국 나는 계속

Refresh Token은 쿠키에 제대로 저장이 되지 않았음 ➡ 30분이 지나면 새로운 Access Token 갱신을 위해 Refresh Token을 서버에 보냄 서버 입장에서 클라이언트에게서 받은 Refresh Token은 자신이 DB에 가지고 있는 Refresh Token이랑 아예 다른 값임 ➡ '토큰 갱신 실패!' 가 콘솔에 찍힘

이 과정을 반복하고 있었던 것이었다.🙄

💥 HTTP 헤더로 전달이 된 Refresh Token이 쿠키에 저장이 되지 않는다! 가 내 문제였다. 

 

확인해보니 아래와 같이 Refresh Token이 HTTP 헤더로 refresh Token이 잘 들어왔고, api 쿠키에도 잘 저장이 되었다. 하지만 브라우저 쿠키로는 저장이 되지 않았다.

HTTP 헤더로 잘 들어온 우리의 리프레시 토큰!
api 쿠키에도 잘 저장이 되어 있다. (편안)
브라우저 쿠키에는 엑세스 쿠키만 덩그러니 들어와있다.


🔨 문제 해결 과정 (feat. 삽질과 고뇌의 연속)

문제 해결 과정에 들어가기 전에, 일단 아래 전개될 글은 약간은 정돈되지 못한 날 것의(...) 글일 수 있음을 알려드린다. 오류가 있을 수도 있고! 결론부터 얘기를 하자면, 내가 생각한 방법대로는 해결을 하지 못했다. 내가 생각한 방법으로는 해결이 안될 것 같아서 차선책으로 솔루션을 생각해보기도 했다. 

 

어쨌든 결론적으로 내가 생각한 방법으로는 해결이 안됐으므로, 차선책으로 생각해본 솔루션을 먼저 말해보겠다.

 

👉 Refresh Token도 Access Token과 마찬가지로 Api response 안에 넣어서 같이 주자!

 

실제로 며칠동안 삽질을 하다가 도저히 안되겠어서 주변 분들한테 물어봤을 때도 이런 식으로 조언을 해줬었다. 여러 글을 찾아보면서 Refresh Token을 response로 같이 주면 보안 상 문제가 되는 게 아닌가 싶었는데, 어떤 친구는 어차피 암호화되어서 오기 때문에 괜찮지 않냐는 식으로 답을 해줬다. 그래도 보안 상 이슈가 되는지 안되는지 확실치 않다..

 

이 방식이 우리 팀에 가장 적합할 솔루션이라고 생각했는데, 데모데이 때까지 서버 측에서 고칠 수 없을 것 같다는 얘기를 해서 실제로 적용해보진 못하고 어쩔 수 없이 원래 적용해보려했던 솔루션을 계속 파보았다.

 


 

그럼 이제 나의 삽질기로 들어가보자! (드디어)

 

문제를 해결하기 위해서 아래와 같은 과정을 시도해보았다. 혹시 다른 사람들도 나와 같은 이슈를 겪고 있다면 이런 부분들을 고려하면 좋을 것 같다.

 

1. 서버에서 쿠키의 속성들을 바꿔주기

이와 관련해서는 이 글을 많이 참고하였다. 구글링을 엄청 하다가 요 글을 발견하고 서버 팀한테 수정을 해줄 수 있냐고 우두두 말을 했었다.. ㅎㅎ 서로 톡을 엄청나게 했었다😹

 

서버에서 쿠키를 보내줄 때는 다양한 옵션을 덧붙일 수 있다. 그 중에 우리 팀이 집중했던 부분은 httpOnly, Samesite, Secure 이렇게 3가지이다. 

 

httpOnly

원래 httpOnly 속성 없이 서버 단에서 클라이언트로 쿠키를 보내게 된다면 클라이언트는 이를 JS로 조회를 할 수 있다. 하지만 이는 곧 제 3자도 마찬가지로 쿠키를 조회할 수 있다는 뜻이다. 이는 보안 상 매우 취약해질 수 있는 부분이기 때문에, httpOnly 속성을 덧붙여 브라우저에서 쿠키를 접근할 수 없도록 제한을 줄 수 있다. 즉, XSS 공격에 취약할 수 있는 부분을 보완해줄 수 있는 보안 방법이라는 것이다.

 

Samesite

SameSite 속성은 서로 다른 도메인 간 쿠키 전송에 대한 보안을 설정해준다. 총 3가지 옵션이 있다.

 

  • None 
    • 동일 사이트에서나, 크로스 사이트에서와 같이 모든 상황에서 쿠키가 전송이 될 수 있다. 하지만 이 때문에 CSRF 공격에는 취약하기 때문에, Secure 속성을 같이 작성해줘야 한다.
  • Strict
    • 가장 강력하게 제한을 두는 설정이다. 소스가 되는 도메인과 대상의 도메인이 일치해야만 쿠키가 포함이 되어 전송이 되는 속성이다.
  • Lax
    • 기존 Strict 속성에서 예외처리가 된 속성이라고 생각하면 된다. <a href>와 같은 앵커 태그나 get 메소드 정도만 쿠키 전송이 허락이 되고, 나머지는 같은 도메인이 아닌 경우에는 전송이 되지 않는다. 

처음에 서버에서 보낸 HTTP 헤더에는 SameSite 설정이 명시적으로 써있지 않았었기 때문에 디폴트 값인 Lax로 설정이 되어있었다. 최근 크롬 정책 변화로 인해 디폴트 값이였던 None 속성이 이제는 Lax가 되었다고 한다. Lax로 설정이 되어 있기 때문에 다른 도메인에 대해서는 쿠키 전송이 제대로 되지 않을 수도 있다고 생각을 했다. 당시 클라이언트는 로컬에서 작업을 하고 있었다. 즉, 서버와는 다른 도메인에서 작업을 하고 있었기 때문에 혹시 이 속성을 none으로 바꿔주고, secure 속성을 넣어주면 되지 않을까 해서 서버한테 바꿔달라고 요청했다.

 

Secure

이는 https 환경에서만 쿠키에 접근할 수 있게 해주는 속성이다. Samesite=none; 이라는 속성을 쓰면 같이 쓰는 것을 권장한다고 해서 우리 팀도 그렇게 수정을 했다. 다만.. 서버의 경우 https에서 작업을 하고 있는 상황이였지만, 클라이언트는 로컬 http에서 작업을 하고 있었다😳 클라이언트 단에서 로컬 http 환경을 https 환경으로 바꾼 이야기는 아래에서 해보겠다.

 

서버 옵션 정리:
httpOnly, Secure, SameSite=None 옵션을 HTTP 헤더 쿠키에 담았다.

 

2. 클라이언트에서 고쳐보기

 

> 로컬 개발환경에 HTTPS 적용하기(윈도우 버전!!)

 

서버에서 쿠키 속성들을 수정해준 후에도 딱히 문제가 해결이 되지 않았었다. 잘되었으면 좋았을텐데..😹

그래서 클라이언트의 로컬 환경에서 http 환경을 > https 환경으로 바꿔보려는 시도를 해보았다. 이런 게 가능한건가? 생각하면서 구글링을 해보았는데 실제로 글들이 많이 나와서 새삼 놀랐다 ㄴㅇㄱ..

 

현재 나는 서버에서 Secure 속성을 사용한 쿠키를 사용하려고 하고 있기 때문에 http인 로컬임에도 불구하고 HTTPS 환경을 사용해야 한다. 하지만 HTTPS를 사용하고 싶다면 TLS/SSL 인증서가 필요하다. 이는 실제 인증기관(Certificate Authority, CA)가 서명을 해줘야 하는 것이다.

 

그러나 CA에서 인증서를 발급받는 것은 로컬에서 시도하기에는 살짝 복잡한 면이 있다고 한다. 또, CA에서부터가 아니라 개인적으로 자체 서명한 인증서를 생성하고 브라우저가 이를 허용하게 할 수도 있다고는 하는데, 이는 경고가 계속 뜨면서 거슬리는 부분이 많다고 한다.

 

그래서 나는 개발 중인 로컬 환경에서 SSL 인증서를 발급해주는 라이브러리 mkcert라는 도구를 사용해보기로 했다. 

 

mkcert 설치하기 at Window

 

mkcert를 설치하는 것은 OS에 따라 방법이 다르다. 찾아보면 맥에서 설치를 하는 방법은 꽤 많이 나오는데, 윈도우에서 설치를 하는 방법은 많이 나오지 않았던 걸로 기억한다. 

 

나는 평생 윈도우만 사용한.. 사람이다..😳 고로 이 설치 과정은 윈도우 기준임을 알린다! 

 

설치하고 실행하면서 좀 많이 헤맸는데, 최대한 기억나는대로 정리를 해서 올려본다. 

 

1. Chocolately 패키지 설치

 

윈도우를 쓰면 한번에 mkcert를 설치하지는 못한다. 패키지 매니저인 chocolately를 이용해서 설치를 하게 된다. 때문에 chocolately를 먼저 설치해주자.

 

👀 잠깐! 설치 전 기본 요구사항 체크

 

  • Windows 7+ / Windows Server 2003+
  • PowerShell v2+ (온라인 설치 시 v3+)
  • .NET Framework 4+ (온라인 설치 시 4.5+)

파워쉘로 설치를 해주는 방법도 있는데, 나는 여기에서 .exe 파일을 수동으로 설치를 해줬다.

 

제대로 설치가 되었다면 관리자 권한으로 실행이 된 cmd에 choco를 입력하면 버전이 나온다.

 

잘 설치가 되었당

 

2. mkcert 설치하기

 

관리자 권한으로 cmd을 실행하여 내가 작업하고 있던 폴더에 mkcert를 설치해주었다. 작업하는 파일의 루트 디렉토리로 이동하고나서 아래 단계를 실행해야 한다!

mkcert -install

그러면 아래와 같이 터미널에 뜨게 된다. 해당 명령은 로컬 인증기관(CA)를 생성해준다. mkcert로 생성한 로컬 CA는 로컬에서만 활용할 수 있다.

다음은 아래 명령어로 인증서를 실제로 만들어준다.

mkcert localhost

그리고 작업하던 프로젝트 코드를 보게 되면 실제로 로컬 인증서가 담겨있다. 주의할 점은 해당 파일은 누구에게도 공유를 해서는 안된다는 것이다. 팀원에게도 공유를 해서는 안되며, 자신의 로컬에서만 사용을 해야 한다.

임시 SSL 인증서가 생겼다~!

3. package.json에서 npm start 스크립트에 SSL 인증서 위치를 설정해준다.

"scripts": {
    "start": "react-scripts start",
    "start:windows": "set HTTPS=true&&set SSL_CRT_FILE=cert/localhost.pem&&set SSL_KEY_FILE=cert/localhost-key.pem&&npm run start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
 },

 

4. React 실행해주기~!!

 

그냥 단순히 npm run을 하니깐 기존 http 환경에서 실행이 되었다.

아래 명령어로 실행을 해주니 https가 제대로 적용이 되었다.

npm run start:windows

저 자물쇠가 보이시나요!! 야호

 

음.. 네.. 그래서 길고 긴 과정을 거쳐 클라이언트 쪽에서 https 환경을 만들어주었다. 그리고 refresh Token이 브라우저 쿠키에 잘 들어갔나 했지만?

 

되지 않았다.. ^-^

 

 

그래서 혹시 서버와 클라이언트 간 도메인이 일치하지 않아서 그런 것일까 싶어 최후의 방법으로 배포를 한 후, 서버와 클라이언트의 도메인을 통일해보았다. 

 

근데 안 됐다...^_^ 

 

 

쁘엥


😷 결론

이런저런 방법을 찾아보면서 며칠을 세우게 되었고, 마감날이 코앞으로 다가왔다. 이제 프로젝트를 점검하고 정리해야할 시간이었기 때문에 더이상 이 문제를 붙들고 있을 수는 없었다..

 

때문에 우선 mvp 단계에서는 서버가  accessToken이 유효한 시간을 늘려 배포하는 것으로 결정을 해 마무리를 지었다. 토큰이 만료가 되면 사용자에게 로그인을 다시 해달라는 alert를 띄우는 식으로 대처를 했다.

 

마지막에 해결이 되었습니다! 라는 사이다 결말이었으면 좋겠지만, 아쉽게도 그런 결말을 맞진 못했다. 서버랑 mvp 단계 이 후 리팩토링을 해보겠다고 얘기를 했었는데, 기회가 된다면 한 번 고쳐보고 싶다. 

이 에러를 해결하려고 하면서 인생 최고의 삽질을 경험하였지만, 그 과정에서 꽤 많은 것을 배운 것 같아 고민 끝에 글을 작성하게 되었다. 나와 비슷한 에러를 겪고 있는 누군가에게 조금이나마 도움이 되었으면 좋겠다!

 

📌 참고 자료

HTTP 헤더 관련 자료

Set-cookie로 브라우저에 쿠키 저장하기

https와 cross domain으로 서버와 통신 시 쿠키 보내는 법

로컬에서 https 적용하기 관련 자료

Windows용 패키지 매니저 chocolatey 설치하기

Windows용 패키지 관리자 Chocolatey(choco) 설치 및 이용하기

https로 React 로컬 테스팅하기

로컬 개발에 HTTPS를 사용하는 방법

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함