회원가입 시 폼을 받거나 카카오 로그인을 하게 만들기로 했다. 하지만 기획된 서비스는 case가 너무 다양했다.
1️⃣작가회원과 일반회원 두 가지로 유저가 나뉘고, 나뉜 유저에 따라 로그인 후 추가적으로 받아야 하는 정보가 있다.
2️⃣ 순서가 복잡하다. 폼으로 받을 수 있는 정보와 카카오 로그인으로 받는 정보가 차이 나기 때문에 이를 고려해야 한다.
(폼 로그인) 일반 회원 → 일반 회원 폼 작성 → 관심지역 설정 → 로그인
(폼 로그인) 작가 회원 → 작가 회원 폼 작성 → 관심지역 설정 → 로그인
(카카오) 일반 회원 → 카카오에서 받지 못한 비밀번호, 이메일(선택이기에 못받을 수 있음)을 받는 폼 작성 → 관심지역 설정 → 로그인
(카카오) 작가 회원 → 카카오에서 받지 못한 정보 폼 작성 → 작가 회원 폼 추가 작성 → 관심지역 설정 → 로그인
이렇게만 봐도 복잡한데 카카오 로그인 시 redirect 되는 문제 때문에 해당 로직이 괜찮은지도 확신할 수 없는 상황이었다.
헷갈려서 적어두지 않으면 못할 정도..
2. 카카오 로그인만 하자!
우리는 4주 프로젝트였기 때문에 로그인에 모든 시간을 쓸 수 없었다. 경우만 다양해지는 방법보단 지역에 기반한 국내 유저를 대상으로 하는 서비스이기 때문에 카카오 로그인만 적용해도 되겠다 싶었다. 내부적으로 회의를 거친 후 과감히(?) 폼을 빼고 카카오 로그인으로 다시 로직을 만들었다.
당시엔 간단한 로직에 감탄하며 좋아했다
1️⃣ 일반 회원 → 회원 유무 확인 → 처음일 경우 관심 지역 설정, 처음이 아니라면 바로 로그인
2️⃣ 작가 회원 → 회원 유무 확인 → 처음일 경우 공방 정보 등록 및 관심지역 설정, 처음이 아니라면 바로 로그인
3. API를 유저 별로 나눠야 할까?
로그인 과정만 생각하면 끝일 줄 알았지만 DB 형태나 OAuth 개념을 생각해야 했다. 우선 기존 기획으론 작가 회원 DB와 일반 회원 DB가 분리되어 있어, 하나의 이메일로 작가 회원 계정을 만들었어도, 일반 회원 DB에 해당 이메일이 없다면, 신규 유저로 가입할 수 있게 했다.
하지만 카카오 로그인만 하는 경우, 같은 서비스인데 다른 redirect URL을 호출하는 것도 불가능하고, 카카오 로그인으로 받는 정보는 똑같은데 이를 분리하는 것도 맞는지 확신이 없었다.
무엇보다 백엔드와 미스 커뮤니케이션이 일어나 백엔드는 이미 하나의 유저 DB에서 관리하는 방식으로 만들어졌다는 것이었다...
그래서 다시 생각해본 결과 카카오 로그인이라는 하나의 OAuth를 거치는 이상 우리 서비스가 구별해야 한다해도 두 개로 나누는 게 어렵다는 결론에 이르렀다. 그리고 이에 대한 해결책으로 작가 회원에게 받는 정보가 일반 회원에게 받는 정보 + 공방 정보이니, 공방 정보를 입력하면 작가 회원으로 업그레이드 시켜주는 방식이 좋겠다는 판단을 했다.
최최최종
4. 누가 카카오랑 통신할래?
1️⃣ (일반적인 방법) 카카오와 프론트가 redirect URL과 token을 받는 방법.
2️⃣ 백엔드가 카카오와 통신하며 token을 받아 해당 토큰을 프론트가 받는 방법.
결론부터 말하자면 우리 팀은 백엔드 통신(2️⃣)을 선택했다. 이유는 프론트에서 token을 관리하고 싶지 않았기 때문이다. token은 유저의 개인 정보를 담고 있기 때문에 token을 해킹당한다면 그 유저의 계정은 해킹당했다고 생각하면 된다. 그렇기 때문에 굳이 code 번호로 통신하는 것보단 애초에 백엔드에서 token을 관리하고 프론트는 암호화된 token만 알고 access, refresh token은 일절 모르길 바랐다.
하지만 이 과정은 인터넷에 잘 나와있지 않아서 백엔드 분과 로그인 얘기를 한참 했다. 주로 고민한 부분은 다음과 같다.
유저가 카카오 로그인을 완료하고 나면 내려주는 token을 백엔드가 갖고 있으면 프론트는 어떻게 유저 정보를 기억할까?
처음엔 api를 호출하면 된다 생각했는데 다시 생각해보니 redirect가 되면 프론트는 아무 정보도 기억하지 못할텐데 무엇으로 api를 요청을 해야하는건가의 의문이 생겼다. 그래서 찾은 방법이 token을 파라미터로 전달하되, 암호된 token을 전달하여 추후 백엔드로 이 token이 다시 전달됐을 때, 백엔드에서 복호화하는 것이다. 프론트는 암호화된 token을 받고, 복호화하는 방법은 백엔드만 알고 있는 것이다. 너무 멋지다.
만약 유저가 정보를 입력하는 중간에 꺼버린다면?
전체적인 로직을 끝내고 나니 세세한 부분이 신경 쓰였다. 만약 유저가 입력 중간에 서비스를 종료한다면 카카오 로그인을 한 후엔 DB에 해당 계정으로 공간이 만들어졌을 텐데, 이에 대한 판단을 어떻게 하면 좋을지 생각했다. 그래서 내린 결론은 백엔드에서 회원가입 페이지를 거칠 때마다 갱신된 유저정보를 내려주는 것이었다. 그럼 프론트에서 없는 정보에 맞춰 라우팅 처리를 해주면 되는 것이다. 백엔드 분이 정보를 쿼리스트링으로 내려주기 때문에 페이지 별로 채워져있는지, 비어있는지를 boolean 값으로 내려주는 것이 좋다고 하셨다.
로그인이 성공했을 경우 백엔드는 Kakao Server에서 받은 Access Token을 통해 UserInfo를 조회함.
백엔드는 별도의 JWT 토큰을 만들어서 Client 측 로그인 완료 페이지로 Redirect 시키면서 QueryString으로 Token을 같이 전달하도록 구현.
프론트는 해당 token과 flag를 추출하여 사용자의 페이지 이동을 처리함.
5. 자동 로그인에 대하여
로그인 로직이 대강 잡히자 완성된 유저정보를 어떻게 관리할지에 대한 고민이 생겼다. 주된 고민은 다음과 같다.
local에 유저정보를 기억해서 자동 로그인을 만들어볼까?
유저 정보를 hook으로 관리할까? recoil 등 상태관리 툴로 관리할까?
첫 번째 고민에 대한 답은 자동 로그인 기능은 배제하자였다. 요새 모든 서비스들이 웬만하면 자동 로그인을 지원하니까 나도 당연히 해야 한다 생각했다. 하지만 자동 로그인을 지원하게 할거냐, 말거냐에 대한 고민 전에 이는 사용자한테 옵션으로 주어져야 한다는 점이었다. 무조건 자동 로그인을 되게 해서는 안되는 거였다. 이렇게 알려주면 당연하지만 나는 local에 저장할까? 상태로 저장할까? 처럼 둘 중 하나만 선택해야 한다는 생각에 갖혀있었다.
앞서 말했다 시피 4개월 프로젝트에서 우선 구현해야 하는 것은 브라우저 창이 꺼지면 자동으로 로그아웃되는 로그인 기능이다. 자동로그인을 사용자로부터 선택받아 local에 저장시켜 놓는건 추후 고민해야 하는 일이었다. 무조건 자동 로그인하게 만들면 유저가 PC방이나 공용 컴퓨터에서 로그인했을 때 그 기록이 그대로 남으니 위험하다. 특히 카카오 로그인은 실제 계정을 사용해야 하는만큼 보안에 대한 많은 고민을 했다.
두 번째에 대한 고민은 hook과 상태관리에 대한 정의를 잘 몰라서 생긴 고민이다. 결론을 말하자면 상태관리로 전역에서 관리하는 게 낫겠다 판단했다. hook은 컴포넌트에 필요한 기능을 지원하고, 전역에서 관리하는 건 상태로 빼서 따로 관리하는 게 나은 것이다.
고민한 포인트는 다음과 같다.
hook에서 로그인 유무를 판단하는 함수와 이에 대한 결과를 같는 변수를 지정하여 호출한다.
유저 정보를 상태로 저장하여 유저 정보가 필요한 곳에서 해당 상태를 호출한다.
둘 다 괜찮은 방법인 것 같아서 고민했지만, 앞서 말한 이유와 같이 상태로 관리하는 게 좋다고 판단했다.
🗒️ 정리
카카오 로그인 시 받을 수 있는 정보가 정해져 있기 때문에 추가로 받아야 하는 정보가 있다면 순서를 잘 고려해야 한다.
카카오 로그인을 하는 전후로 redirect가 되어 정보를 기억하지 못한다는 점을 명심하자.
카카오 로그인을 프론트, 백 중 누구와 할지에 따라 방식이 달라진다.
프론트와 통신 시 redirect URL과 code로 token을 노출시키지 않고 통신한다.
백과 통신시 추후 프론트에게 token을 전달할 때 암호화를 해야 한다.
대부분의 페이지에서 유저 정보를 가지고 판단을 해야할 경우(전역에서 관리가 필요한 경우) 상태로 관리하는 것이 좋다.
로그인 만만하게 보진 않았지만 그래도 어렵다. 코드 좀 치게 해주세여...
👊 이제 구현해볼까?
Login
일반 회원으로 선택했는지, 작가 회원으로 선택했는지 구별을 위해 session storage에 저장.
API base URI를 .env 파일에 저장하고 이 외 필요한 URI는 변수로 지정함.
isAuthenticated로 이미 로그인을 한 유저라면 바로 홈페이지로 이동.
버튼을 누르면 카카오 로그인 페이지로 redirect됨.
이때 카카오 로그인 페이지라는 외부의 link로 연결시키기 위해 a태그 사용.
router는 내부 링크로 이동하기 때문에 사용할 수 없음.
강제로 이동하려는 접근이 보이면 카카오 쪽에서 block을 시키는 경우도 있음.
RedirectPageAfterKakao
사용자가 카카오 로그인을 하면 백엔드에서 전달 받은 암호화 토큰과 회원가입에 필요한 폼이 적혀있는지 유무를 판단하는 flag를 쿼리스트링으로 전달받음.
searchParams를 이용하여 &로 구분된 요소를 찾아 각 변수로 지정.
그리고 앞서 session storage에 저장한 role을 가져옴.
이렇게 ROLE과 폼을 채워하는 부분을 파악해 유저의 페이지를 라우팅 처리함.
각 폼마다 flag를 받는 이유는 만약 유저가 회원가입 중간에 페이지를 나갈 경우를 고려했기 때문.
이미 제출한 폼은 다시 작성할 필요없이 flag가 true로 지정되어 있기 때문에 해당 페이지로 이동시키는 게 아닌, 그 다음 페이지로 이동시킴.
😱
헤맨 점
- token이 빈 값으로 담기는 현상 발생.
- 분명 prams로는 잘 담겼지만 빈 값이 계속 담김 → token을 받아 state를 갱신하는데 이때 리렌더링이 되며 쿼리스트링을 참조할 수 없게 됨.
- useEffect에 담아 최초 실행시에 token을 쿼리 스트링으로 부터 받음 → 리렌더링이 되어도 token을 쿼리스트링에서 다시 받으려고 하지 않아 기존에 담긴 값을 유지할 수 있음.
💡
구현 포인트
- ROLE에 따라, 그리고 flag에 따라 페이지 이동을 결정하기 때문에 if문이 많이 사용됨.
ex) ROLE_USER이고, isFillAddressInfo가 false이면...
- 또한 if문으로만 작성시 페이지를 push할 때 return으로 빈 값을 반환해줘야 함.
- 가독성이 떨어질 것을 고려해 switch문과 삼항 연산자를 같이 사용함.
- switch를 사용하여 두 가지 case를 먼저 정의하고, USER의 경우 두 가지의 경우의 수가 있기 때문에 삼항 연산자로 표현함.
PrivatRouter와 PublicRouter 구별
privateRoute는 RouteProps를 상속받아 Route를 할 수 있게 함.
isAuthenticated일 경우 children을 render하도록 함.
state로 이전 경로를 기억해 로그인 했을 때 다시 있었던 페이지로 이동시킴.
🥲 아쉬운 점
atom에 저장한 userState에 token을 담은 상태로 기능을 구현함.
하지만 userState에 token을 저장하는 게 아닌, auth 관련 atom이 별도로 존재해 로그인 유뮤를 확인하는 isAuthenticated 상태와 token이 별도로 존재했어야 함.