프리온보딩 FE 챌린지 10월 - 로그인 기능 구현, 하나부터 열까지! 강의를 들으면서 내가 만든 개인 프로젝트의 로그인 로직이 계속 신경 쓰였다. 만들고 싶었던 기능에만 치중해 기본이면서도 가장 중요한 로그인을 대충 만들었기 때문이다. 얼마나 대충 만들었는지 보면 깜짝 놀라리라.
아무튼 강의에서 들은 내용을 참고해서 봐주기 힘든 로그인 과정을 그나마 볼 만한 모양새로 고친 과정을 기록해 보기로 한다.
로그인의 목적은 유저에게 서비스 이용 권한이 있는지 식별하고, 유저의 행위를 제어하고 관리하기 위함이다. 등록되지 않은 유저가 서비스의 CRUD 요청을 멋대로 해서는 안 되고, 권한이 없는 페이지에 접근해서도 안 된다.
식별된 유저라면 서비스를 계속 이용할 수 있도록 인증 상태를 유지해야 한다. 로그인하고 A서비스 이용하고, 로그인하고 B서비스 이용하는 식이라면... 누구도 이용하지 않는 서비스가 될 것이다.
나 역시 이러한 점을 고려해 로그인을 구현했다. 표면적으로는 말이다.
초기 로직을 한 눈에 보기 위해 시퀀스 다이어그램을 그려봤다.
엉망진창이지만 생애최초 다이어그램이니 넘어가자.
먼저 로그인 유지는 다음 과정을 따랐다.
/auth/check
로 식별을 요청한다.로그인 과정은 다음과 같다.
/auth/google/callback
으로 이동하는 창을 연다./auth/login
요청을 보내 세션에 담긴 유저 정보를 응답에 담아 클라이언트로 보낸다.id
(!!)를 로컬 스토리지에 토큰으로 저장하고 home
으로 푸시한다.누군가 옆에서 봤다면 곧장 '으악!'했을 로직이다. 문제 되는 부분을 스스로 짚어 보자.
먼저 유저의 정보라고 할 수 있는 부분을 암호화도 하지 않은 채 로컬 스토리지에 저장한 것이 가장 큰 문제였다. 프로필에 담긴 속성 중 sub
를 사용했다. 고유값이긴 하지만 숫자로만 되어 있어서 별로 중요하지 않다고 판단했기 때문이었다. 프로젝트 자체도 털릴 게 없었고.
하지만 이 자체가 위험한 생각이었다. 보안 측면에서 보면 그 내용이 뭐가 되었든 유저의 정보에 해당한다면 숨기는 게 맞았다. 애초에 uuid 등으로 id를 해시값으로 만들었다면 문제가 덜했겠지만, 아무튼 프론트엔드 개발만 생각해 서버쪽은 대충 구현했다.
로그인 유지를 위해서 초기에 세션 방식으로 구현했다. 잘못된 선택은 아니었지만, 그렇다고 특별한 이유가 있지도 않았다. 나아가 REST
한 방식으로 API를 구현했다고 생각했는데, 따지고 보면 세션은 REST
하지 않은 느낌이 들었다.
REST API
의 원칙 중 하나는 클라이언트와 서버 간의 stateless
이다. 클라이언트가 서버의 상태를 가지면 안 되고, 서버도 클라이언트의 상태를 가지고 있으면 안 된다. 그러나 세션 방식은 만료 기한이 있다손 치더라도 서버가 클라이언트의 정보를 가지고 있는 셈이다. 내 생각과 구현을 일치시키기 위해서 세션 방식을 쳐냈다.
변경할 방식을 이렇게 저렇게 생각하다 보니 login
요청도 말이 안 됐다. 페이지 랜딩 시 유저 정보를 검사하는 함수를 실행한다. 로그인 시도 후 페이지로 돌아오면 로그인 함수도 실행되고, 검사 함수도 실행된다. 어쩌다 이런 멍청한 코드를 짠 건지 의문이 드는 대목이었다. 지금도 멍청하지만, 예전에는 더했다. 이것도 고치기로 했다.
이 문제들에 중점을 두고 로직을 변경하기 시작했다.
변경한 로직의 시퀀스 다이어그램이다. 수정한 부분을 중점으로 보자.
세션 방식을 버리고 refresh token
과 access token
을 사용하는 토큰 방식을 채용했다. 구글 로그인을 요청하면 성공 콜백함수에서 refresh token
을 생성하여 응답 쿠키에 담아 보낸다. 이후 로그인 페이지로 redirection
되고, 서버에 유저 정보를 체크하는 요청을 보내는 check
함수가 실행된다.
서버에서는 헤더의 Authorization
에 담아 보낸 Bearer access token
을 확인한다. access token
이 유효하면 유저 정보와 기존 토큰을 응답한다. 만약 access token
이 없거나 유효하지 않다면 쿠키에 담긴 refresh token
을 검사한다. 유효하다면 새로운 access token
을 발급하지만, 이것마저 유효하지 않다면 에러을 응답한다.
발급 받은 access token
은 이전과 마찬가지로 로컬 스토리지에 저장했다. 차이점은 유저 식별 정보가 암호화되었다는 것과 토큰에 만료 기한이 있어 영구 보관이 되지 않는다는 점이다. 이러한 check
과정이 모든 페이지에서 실행되도록 했다. 덕분에 위에서 언급한 불필요한 로그인 요청을 제거할 수 있었다.
코드는 크게 의미 있는 것 같지 않고, 너무 못생겨서 첨부하지 않았다. 코드 보다 로직을 이해하는 게 더 중요하기도 하고.
간단하게만 생각했던 로그인이 전혀 간단하지 않음을 깨달았다. 그나마 내 프로젝트 규모가 작아서 사용자 식별만 생각하면 되는 것이지, 다른 큰 서비스처럼 admin이 나뉘고, 다른 도메인과 연결하고 하다 보면 진짜 어마어마하게 복잡할 것이다.
또 서버 코드를 직접 변경하면서 어떤 식으로 요청을 주고 받는지 더 깊게 이해할 수 있었다. 이전에는 단순하게 클라이언트에서 요청을 보내면 서버에서 그 요청에 해당하는 응답만 해주면 된다고 생각했다. 그래서 변경 전 로직이 그 모양이었고. 이제는 내가 어떻게 요청을 보내야 서버에서 어떤 응답을 하는지 사전에 고민해 보고, 적절한 에러핸들링까지 포함한 코드를 작성할 수 있을 것 같다.
로그인 뿐만 아니라 본 서비스에 대한 요청과 처리도 이번에 배운점을 이용해서 더 나은 코드로 리팩토링해 봐야겠다. 로직과 코드가 일치하는 개발자를 목표로 하자.