* 로그인을 처음 구현해보는 코린이의 글입니다.
검증되지 않은 뇌피셜이 포함되어 있을 수 있습니다.
💜 리액트에서 유저의 로그인 상태를 어떻게 관리해야할까?
이번에 회원가입, 로그인 기능을 처음 구현해보면서 이런 부분은 어떻게 해결해야하지?...ㅎㅎ
여기서 이러이러한 예외가 발생할 수도 있겠네, 그렇다면 어떻게 처리하지?하는 고민에 휩싸이고 말았다.
일부 페이지는 로그인한 유저만 접근할 수 있도록 라우트를 설정해야 했다.
sessionStorage의 token이 존재하는지 여부에 따라 접근 권한을 확인하도록 작성했다.
접근 권한의 문제는 간단히 해결되었지만, 문제는 userToken이라는 key에 value가 아무거나 들어가 있어도 페이지 접근 권한이 열린다는 치명적인 문제점이 있어서 개선이 필요하다. 물론 토큰이 유효하지 않기 때문에 서버와의 통신 에러는 발생하겠지만, 로그인 유저만 입장할 수 있는 페이지를 아무나 들어갈 수 있다는 모순은 해결해야 한다.
이건 토큰을 디코드해서 받은 Role 값을 가지고 유저 유형에 따라 접근 가능한 페이지를 나누는 방법으로 수정할 예정인데,
만약 이 부분에서도 문제가 발견된다면 다른 방법을 모색하고자 한다.
오늘은 이거보다 더 중요한 것이 있어서 다른 부분부터 먼저 개선할 생각이다.
💜 JWT 디코드, 해도 될까?
해도 될까? 당연히 토큰을 디코드 하는 것은 문제가 되지 않는다.
일단 JWT는 복호화가 쉽기 때문에 유저의 중요한 정보는 담지 않는 것이 좋다. (예를 들면 패스워드라던가, 패스워드라던가..)
사실 이전 프로젝트에서 토큰을 복호화 했을 때 유저의 비밀번호도 함께 넘겨주셔서 깜짝 놀랐던 적이 있다.. 하하.. 😂
JWT 토큰에 유저의 일부 정보가 담겨 있는데 이를 사용하기 위해 디코드를 진행했다.
디코드해서 전역적으로 토큰에 담긴 유저 정보를 사용해야할 필요가 있었는데 디코드를 여러 차례 수행하는 경우 성능 이슈가 발생할 것으로 생각했다. 물론 당장 엄청난 성능 이슈는 아니겠지만, 디코드를 불필요하게 여러번 호출할 필요가 없다면 최소화 하는 것이 좋기 때문이다.
토큰이 변경될때만 디코드를 수행하도록 코드를 수정하고,
디코드된 정보를 전역적으로 사용해야 할 것 같아 contextAPI를 사용하기로 했다.
먼저 토큰을 디코드하기 위해 사용한 jwt-decode 라이브러리를 설치했다.
토큰을 인자로 받은 후 디코드를 실행하는데 만약 실패했다면 에러를 발생시키고 null을 리턴하는 함수이다.
디코드에 성공하면 해당 토큰을 디코드하여 리턴시킨다.
💜 JWT 디코드를 하게 된 계기
팀원 분께서 사용자 닉네임이 서버로부터 넘어오지 않는다는 말씀에 '토큰을 디코드해서 사용하면 되겠다'는 생각이 먼저 들었지만,
해당 지식에 빠삭하지 않았기 때문에 서버에서 유저 필요한 일부 데이터를 받아오는 게 더 나은 선택인지, 디코드를 해서 전역적으로 사용하는 게 더 나을지에 대한 고민을 하게 되었다.
서버와 여러차례 통신하는 것보다는 디코드 한차례 하는 게 더 낫지 않을까..?
아니면 팀원 분의 말씀처럼 로그인에 성공했을 때 닉네임을 한차례 받아오는 게 나은 방법일까?
(하지만 닉네임 외 다른 정보가 필요할 경우가 생긴다면?)
1. 만약, 매 페이지마다 유저 정보를 받아온다고 가정한다면,
네트워크 성능면에서 유저 정보를 매 페이지마다 받아오는 것도 굉장히 비효율적일 것 같다는 생각이 들었고
2. 그렇다고 로그인할 때 유저 정보를 넘겨받는 것도 문제가 있지 않나?를 고민해봤을 때
당장은 닉네임일지 몰라도 또 다른 정보가 필요할 경우 보안상으로 이슈가 있을 수 있을 수 있겠다고 생각을 했다.
3. 역시 토큰을 디코드 하는 게 제일 베스트일까?
결국 어차피 유저 정보는 서버에서 전달해줘야 하므로 요청하지 않으면 받아 쓸 수 없다.
(현재는 토큰을 복호화 하는 방법뿐. 음. 답정너였나.. 😗)
닉네임을 받아올 수 있는 경로가 토큰을 복호화해서 꺼내 쓰는 방법밖에 없었으므로 가능한 방법을 채택했다.
만약 JWT 토큰을 디코드하는 것이 더 좋지 않다고 한다면 해당 부분은 언제든 개선할 생각이다.
물론 백엔드 팀원 분께 보내달라고 요청해야겠지만 ㅋㅋㅋ
리프레시 토큰을 백엔드에서 준비를 해주셔서 어차피 액세스 토큰의 만료를 확인해야 하는데 액세스 토큰의 유효 시간을 1시간으로 설정해두셨다고 들었던 것 같다.
리프레시 토큰때문에라도 조금 전에 진행한 토큰 디코드는 어차피 필요했었던 과정임을 알게 되었다. (리프레시 토큰 그게 뭐죵😢)
리프레시 토큰과 관련해서도 할 말이 많지만, 당장은 useAuth 커스텀 훅에 대해서 주절거려야 하므로 잠시 패스.
💜 다음은 토큰과 관련한 커스텀 훅 useAuth이다.
현재는 decodedUserToken이라는 함수를 export해서 디코드 된 토큰이 필요한 모든 페이지에서 import후 호출하는 방식으로 사용해야 한다.
코드는 위와 같다. 토큰과 관련해서 사용하려고 했지만 디코드 하는 부분에서 이슈가 발생했다.
우선 이 커스텀 훅을 사용해 디코드 토큰을 사용하려면 다음과 같이 호출해야 한다.
문제점 인지
decodedUserToken은 함수이기 때문에 export한 이후에도 한차례 호출해야하는 불편함도 있었고,
함수 내부에서 디코드를 수행하는 라이브러리 함수를 호출하고 있기 때문에 불필요하게 여러 차례 디코드를 수행하므로 성능을 고려하지 못한 설계였던 것 같다..고 생각한다! 😂
넘나 어려운 코딩세계.
완전히 잘 고친 코드는 절대로 아닐테지만, 나름대로 고민해서 짰다.. 흑흑
블로그 작성하면서 보니 리팩토링이 필요할 것 같은 부분으로 useEffect가 눈에 띄는데, 아직 코린이라 직접 코드 넣어보고 동작을 봐야해서 섣부른 판단일 수도 있으니 일단 나중에 리팩토링해보고 동작하는지 확인해볼 생각이다.
하지만 내 새로운 고민을 해결하는 과정에서 리팩토링을 하기도 전에 해당 코드는 삭제했다. (미국에 간 걸로)
지금은 제거된 코드긴 하지만, 나름 해당 코드에 대한 내용을 설명하자면 먼저 userDecodedToken라는 state 변수를 하나 만들고, 이 변수는 디코드된 토큰을 담도록 했다. useEffect에서 userToken이 변경되는 것을 감지하도록 종속성 배열에 추가했다.
처음 렌더링 됐을 때 세션스토리지에서 userToken이 있으면 token 변수에 담을 것이고, setUserToken에서 userToken의 값을 null에서 갱신시킬 것이다. 그리고 종속성 배열의 userToken이 존재한다면 decodedUserToken 함수가 호출되면서 토큰을 디코드 시킨 후, userDecodedToken에 디코드 토큰이 담기게 된다.
이제 useAuth를 사용하면 되겠군!! 하고 좋아했었지만,
다음에 발생한 나의 새로운 고민으로 인해 useAuth를 완전히 갈아치우는 사태가 발생했다.
일단 커스텀 훅과 contextAPI에 대해 무지했던 것도 있어서 문제의 해결 방안을 도출하기까지 굉장히 오래 걸렸다...
하아..할 게 산더미인데 이거 하나 잡고 있다가 소중한 시간이 훅 가버렸다.
커스텀 훅은 디코드 하는 부분을 제거한 부분만 남겨두었다.
리프레시 토큰과 관련한 로직을 작성할 때 쓸 수도 있지 않을까..?해서 전부 다 삭제는 하지 않고 디코드 부분을 제외한 코드만 남겨두고 일단은 킵...
해당 함수들은 컨텍스트 API와 이름이 헷갈릴 것 같아서 다음과 같이 수정했다.
좀 길긴해도 명확한 편이 낫다고 생각한다.
💜 세션 스토리지에서 토큰을 강제로 삭제한다면 어떻게 추적해야 하나?
이 부분 때문에 오늘로 3일 째 해당 이슈에서 막혀서 도르마무.. 무한의 인피니트를 겪고 있다.
어떻게든 혼자 해결해보고 싶어서 아득바득 삽질하고 있었는데 간과하고 있었던 부분이 있었던 것이다. (아........ 밥오..)
이번 이슈는 세션 스토리지에서 유저 토큰을 강제 삭제했을 때는 어떻게 되는가?라는 생각에서 출발하여 끝없는 삽질을 하고 있었다.
누가 그렇게 삭제를 하죠?
라던가, 그런거 까지 고려 안해도 됩니다..와 같은 굉장히 머리 싸매고 고민하고 있는 이슈에 대해서 허탈한 답이 돌아올지도 모르겠다.
실제로 아는 바가 없으니 그게 정답일지도 모르겠지만, 본인은 가능한 한 모든 예외를 찾고 해결하고 싶어하는 고집이 있다.
해결하면 베스트, 적어도 내가 찾아보고 생각한 모든 방법을 다 썼을 때도 안됐을 때 그때 타협하고 싶었다.
그 전까지는 시간을 갈아 넣더라도 해결해보고 싶었는데 구글링에 뭐라고 검색해야되는지도 모르겠다.
세션 스토리지에서 토큰을 강제 삭제하면..과 같은 키워드를 넣어도 원하는 문서가 없길래 아 모른다! 일단 해보자! 가 되었다. 😂😂
문제 인식
세션 스토리지에서 userToken을 강제로 지웠을 때 페이지 접근 부분에서 문제가 발생했다.
세션 스토리지의 userToken을 지우면 마이페이지와 같이 접근 권한이 없는 페이지의 입장을 막고 싶었는데 리로드하기 전까지 상태 값이 세션스토리지의 userToken 키의 변경 사항을 추적하고 있지 않기 때문에 나같은 유저가 강제로 삭제했을 때를 인지하지 못하므로 예외 처리할 수 없다는 문제가 있었다.
내가 코드를 잘못 짠 건가 싶어서 애꿎은 컨텍스트 API와 커스텀 hook만 하루종일 고치고 '않이 외않되????'를 남발하고 있었지만,
앗... 이것은 '세션 스토리지 key 값'
즉, 세션스토리지는 웹 브라우저의 것이기에 리액트에서 제어할 수 없던 부분임을 생각하지 못했다.
그것도 모르고 무한 렌더링을 발생 시킨다거나, 결과가 같은 코드를 생긴 것만 다른 방식으로 여러 차례 작성하고 있었던 과거의 나..
많이 힘들었겠다..하하.. 앞으로도 힘들겠네.. 깔깔..
그렇다면 이런 방법을 써야할까?
브라우저의 내장 API인 sessionStorage를 추적하기 위해서는 1. interval을 사용해서 세션스토리지에 토큰이 있는지 주기적으로 확인을 하거나, 2. storageEvent를 사용하는 방법 두 가지를 떠올릴 수 있었다.
setInterval을 n초마다 토큰이 있는지 확인하는 것은 성능상 문제가 될 수도 있을 것 같고 몇 초를 설정하는 지도 중요할 것 같아서 해당 방법은 차선책으로 남겨놓고 스토리지 이벤트 먼저 사용해보기로 했다.
window에 storage 이벤트를 사용하면 세션 스토리지의 토큰을 강제로 삭제했을 때 문제가 해결될까?
코드에 대한 설명을 주석으로 적어놨으니 자세한 내용은 생략한다.
아무튼 이 코드가 없을 때 동작하는 것과 있을 때 동작의 차이점을 콘솔로 확인해보자.
우선 내가 원했던 동작은 다음과 같다. 편의상 콘솔 출력으로 대체한다.
- 올바른 userToken이 존재하는 경우 디코드된 내용이 콘솔에 출력된다.
- 세션스토리지에서 userToken을 강제로 제거하는 경우 null이 출력된다.
1. 스토리지를 감시하는 이벤트가 없을 때 동작
세션스토리지에 토큰이 존재할 때 디코드 토큰이 출력되는 것은 올바른 동작이다.
하지만, 세션 스토리지에서 userToken을 강제로 제거했을 때도 이 내용이 동기화되지 않고 디코드 토큰 내용을 계속 사용하고 있다.
2. 스토리지를 감시하는 이벤트를 추가했을 때 동작
내가 원하던 결과가 바로 이거였다.
웹 스토리지 부분을 간과하고 다른데서 시간을 쏟았었지...ㅋㅋㅋㅋㅋㅋ
로그인하면 복호화한 토큰이, userToken을 강제로 지웠을 때 null을 출력하는 것을 보니 제대로 동작한다.
3일 간의 고통속에서 해방되는 순간이다...ㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠ
💜 로그인 페이지에도 문제가 있었다.
거의 원하는 방향에 근접하고 있었을 때 세션스토리지에 처음 토큰 값을 저장하는 부분을 간과하고 있었다.
스토리지 이벤트를 통해 거의 다 도착했다고 생각했는데, 세션스토리지에 userToken이라는 key가 없는데 페이지에서는 정상적으로 디코드 토큰을 쓰고 있다. 물론 아직 코드가 새롭게 반영되지 않은 '마이페이지'같은 경우는 userToken이 세션 스토리지에 존재해야만 접근이 가능했기에 자꾸 로그인 페이지로 라우팅되는 것이 이상해서 찾게 된 문제였다. 🤣
뭐야 무서워..하고 있었는데 가만 생각해보니 로그인 페이지가 세션 스토리지에 저장을 하는 역할을 한다는 것을 잊고 있었다.
단순히 세션 스토리지의 토큰의 존재 여부에 대해서만 계속 신경썼던 탓에 로그인 페이지에서 발생한 문제를 뒤늦게 발견했다.
세션 스토리지에 토큰이 존재할 때도 왜 null이지, 동기화가 왜 안되었지?에 대해서 초점을 맞춘 탓에 로그인 페이지를 검토할 생각을 못 하고 있었다.
우선, '토큰이 어디에서 오는가' 즉, 어디서 시작되는가를 먼저 생각했어야 했는데...
가장 먼저 로그인에 성공하면 서버로부터 토큰을 받아오고 이 토큰을 처음 스토리지에 저장하는 것도 응답 코드 200을 받았을 때이다. 로그인 페이지에서 응답코드 200을 받고 setItem 메서드를 호출하여 userToken이라는 key값으로 액세스 토큰을 저장하고 있다.
여기서 출발했어야 했는데 애꿎은 context API와 useAuth 커스텀 훅만 괴롭히고 있었으니.. 어휴.. 다음엔 그러지 말자..
(물론 세션스토리지를 리액트로 감지하려고 했던 바보짓이 더 컸지만ㅋㅋㅋㅋ)
useAuth 커스텀 훅에서 세션스토리지에 토큰을 세팅하는 코드가 있기 때문에 다음과 같이 필요한 부분을 import 해왔다.
그리고 로그인에 성공했을 때 해당 값으로 토큰을 세션 스토리지에 저장시키도록 했고
전역 사용을 위해 컨텍스트 API의 setToken 함수에도 전달하도록 했다.
이렇게되면 initSessionStorageUserToken 함수가 세션스토리지에 토큰을 저장하고,
전역으로 사용할 수 있도록 token은 contextAPI의 setState 값으로 전달된다.
결론은 해결했다.. 그리고 전역에서 디코드 토큰을 contextAPI를 사용해서 쓸 수도 있으며,
이상한 사용자(는 바로 나..!)가 userToken을 세션스토리지에서 손수 지웠을 때의 예외처리도 해결했다.
작성한 컨텍스트 API 전체 코드는 다음과 같다.
하도 코드 수정을 많이 해서 불필요한 코드가 섞여들어가 있을지도 모르겠다.. 깔깔..
아무튼 초보자가 보스 첫 격파한 기분이라 잠은 못잤는데 기분은 행복🥰 이래서 코딩하는군
'오늘의 개발' 카테고리의 다른 글
로딩 컴포넌트(스피너) 적용을 위해 리액트 서스펜스(Suspense), 커스텀 훅스 사용해보기 (0) | 2023.04.13 |
---|---|
리액트에서 소셜 로그인(카카오) REST API 사용하여 구현하기 (0) | 2023.04.11 |
인풋 인터랙션 적용시켜보기 (feat. 인터랙션 욕심이 부른 버그) (0) | 2023.03.30 |
프론트에서 API 문서 작성을?! 백엔드를 위한 문서 작성해보기 (1) | 2023.03.24 |
웹 접근성에 대해 고민해보고 적용해보기 - tabindex (0) | 2023.03.22 |
댓글