아마 아무나 붙잡고 '로그인이 무엇인지 아느냐'라고 묻는다면 모두가 '안다'라고 답할 것이다. 우리는 평소에도 숨쉬듯이 로그인이라는 단어를 말하고, 실제로 의사소통에 큰 무리가 없을 정도로 로그인이 무엇인지에 대해 '이해'하고 있다. 하지만 정확한 로그인의 정의를 말해보라 했을 때 명확하게 답할 수 있는 사람은 그리 많지 않을 것이라고 감히 예상한다. 이런 취지에서 이번에는 로그인의 정의는 무엇인지, 어떤 문제를 해결하기 위해 등장했는지, 그리고 기술적으로 어떻게 로그인을 구현하는지에 대한 개념을 글로써 정리해보고자 한다.
로그인이라는 개념이 없는 가상의 쇼핑몰 A를 상상해보자.
A 쇼핑몰의 옷들이 마음에 들었던 나는 옷을 살때마다 이곳을 애용하고 있다. 그런데 사용할 때마다 여간 불편한게 아니다. 나는 몇년째 같은 곳에 살고 있음에도, 옷을 주문할 때마다 매번 집 주소와 연락처를 처음부터 입력해야 했던 것이다. 게다가 주문은 제대로 들어갔는지, 배송은 어디까지 진행됐는지 확인하기도 힘들고, 이미 주문한 옷의 옵션을 변경하기는 사실상 불가능에 가까워 그냥 포기했던 적도 한두번이 아니었다.
위 예시와 같은 문제에 대응하고자 배송지 정보와 주문 정보를 모든 사람이 열람할 수 있게 서비스에 공개한다면 개인정보 보호 관련 이슈가 생길 수 있고, 수많은 정보들 사이에서 내 정보를 찾는 것도 쉽지만은 않을 것이다. 이처럼 특정 회원에게 귀속된 정보를 안전하고 효과적으로 보여주기 위해 '회원가입'이라는 계정생성 단계가 생겨났다.
A 쇼핑몰은 머리를 싸맨 끝에 주문 전에 고객들이 계정을 생성할 수 있게 기능을 업데이트했다. 이제 고객들은 주문하기 전에 아이디와 비밀번호를 입력하는 것으로 저장된 배송지를 자동입력할 수 있게 되었고, 주문 정보를 확인하고 옵션을 변경하는 것도 주문 조회 화면에서 아이디와 비밀번호를 입력해서 해결할 수 있게 되었다.
우리는 회원가입을 통해 계정을 생성했고, 각 계정마다 고유한 아이디와 비밀번호를 갖고 있다. 그리고 비밀번호는 계정의 주인 말고는 알 수 없으므로, 올바른 아이디와 비밀번호를 입력해 인증 과정을 거친 사용자를 해당 계정의 주인이라고 판단할 수 있게 된다. 이제 결제라든가, 후기 작성이라든가, 회원정보 수정같이 계정의 소유자만 사용할 수 있는 기능 관련 api를 요청할 때마다 회원의 아이디, 비밀번호를 입력받아 api에 실어 보내면 모든 문제가 해결되는 것처럼 보인다.
하지만 사실 이 방법은 언뜻 생각해봐도 꽤나 비효율적이라는 사실을 알 수 있다. 앱을 사용하면서 계정 소유자만 사용할 수 있는 기능이 한두개가 아닐텐데, 그때마다 아이디 비밀번호를 매번 입력하게 한다는건 엄청난 사용성 저해로 이어질 수 있기 때문이다. 그러면 기기에 아이디 비밀번호를 저장해서 필요할 때마다 자동으로 입력하게 할 수도 있지 않을까? 이 방법은 사용성 저해를 유발하진 않겠지만, 저장소에 비밀번호를 직접 저장해서 사용한다는 접근은 보안적으로 치명적인 문제를 유발할 우려가 있다.
이 문제를 해결하기 위해 똑똑한 사람들은 Access Token이라는 개념을 만들어 냈다. 엑세스 토큰은 쉽게 말해서 '내가 이 기능에 접근할 수 있는 권한이 있습니다.' 라고 하는 인증서다. 매번 아이디와 비밀번호를 입력하는 대신, 한번만 아이디와 비밀번호를 제출하면 서버는 그 결과로 엑세스 토큰을 반환해 준다. 그리고 다음에 api 요청을 할때 마다 엑세스 토큰을 같이 보내면 서버는 '아 예전에 그 계정으로 로그인했던 사용자구나'라고 판단한 뒤 소유자와 동일한 권한을 사용할 수 있게 해준다. 페스티벌에서 매번 티켓과 신분증을 확인하는 대신 손목에 입장권을 달아주는 것과 비슷한 논리라고 이해하면 쉽다. 만약 저장 중인 엑세스 토큰이 누군가에 의해 탈취된다 하더라도 토큰을 만료 시킨 뒤 다시 발급하기만 하면 되므로 해킹에도 비교적 손쉽게 대응할 수 있다. 즉, 로그인이란 서비스 계정의 Access Token을 발급해서 그것을 소유하고 있는 상태라고 정의할 수 있다.
사진출처: https://m.blog.naver.com/thenemolab/221525394810매번 귀찮게 아이디와 비밀번호를 입력해야 한다는 CS에 대응하기 위해 A 쇼핑몰은 최초에 아이디와 비밀번호를 입력받으면 그 결과로 Access Token을 반환해주고, 이를 변수에 저장하여 모든 api 요청에 자동으로 넣어주는 것으로 문제를 해결했다. 이제 사용자들은 처음 쇼핑몰에 접속해서 아이디와 비밀번호를 1회 입력하기만 하면 해당 계정의 모든 기능에 접근할 수 있게 되었다. 결제와 주문조회가 쉬워짐으로 매출이 상승한 것은 당연한 수순이었다.
로그인이 유지된다는 것은 곧 엑세스 토큰을 소유하고 있는 상태로 해석될 수 있다고 했다. 그러면 로그인 api의 결과값으로 받은 토큰을 특정한 변수에 저장하고 필요할 때마다 사용하기만 하면 로그인 유지는 달성되는 것이다. 이제 사용자들은 로그인 상태가 유지되는 동안 어떠한 제약도 없이 계정 권한을 소유하게 되었다. 하지만 여기에도 맹점은 존재한다. 바로 token이 저장되는 variable이나 state는 앱이 재시작할 때마다 초기화된다는 점이다. 즉, 앱을 종료하기 전까지는 로그인이 유지되지만, 앱 재시작하면 로그인 정보가 초기화되어 다시 아이디와 비밀번호를 입력해야한다. 서비스의 성격에 따라 앱을 종료하면 로그인 정보를 초기화하고 싶을 수도 있지만, 경우에 따라서는 굳이 로그아웃하기 전에는 로그인을 유지하고 싶을 수 있다.
이를 달성하기 위해 보통 local storage를 사용하게 된다.(웹의 경우 cookie 또는 session storage라는 선택지도 있다.) 로컬스토리지는 말 그대로 기기의 저장소를 의미한다. 앱이 실행될 때 함께 초기화되는 변수가 아니라 기기에 직접 저장되므로 앱의 상태에 따라 휘발되지 않고 값을 유지할 수 있다. 즉, 로그인 성공 결과로 받은 엑세스토큰을 로컬스토리지에 저장하고, 앱을 시작할 때마다 로컬스토리지에 엑세스토큰이 있는지 확인한 뒤 만약 엑세스토큰이 있다면 즉시 로그인 시키는 원리다.
얼마전 A 쇼핑몰에 이제는 앱을 종료했다 재실행해도 로그인이 유지된다는 공지가 올라왔다. 어차피 내 컴퓨터와 내 스마트폰으로 A 쇼핑몰에 접속하는데도 매번 아이디와 비밀번호를 입력해야한다는 사실이 내심 거슬리던 차에 반가운 공지사항이었다. 아이디와 비밀번호를 입력한지 오래 지나서 이제 내 비밀번호가 뭐였는지 가물가물해지기 시작했다.
(그러나 그때쯤 알게된 B 쇼핑몰이 사용성은 좋지 않지만 옷 퀄리티가 A 쇼핑몰보다 월등히 좋다는 것을 깨닫고 나서 B 쇼핑몰로 갈아탔다. 역시 쇼핑몰의 본질은 좋은 옷이지😀)
이번에는 간단하게 React Native로 로그인 유지 예시 코드를 작성해보려고 한다. 에러 처리와 api 응답 체크, 상태관리 등의 코드는 최대한 생략하고 로그인 유지의 전체적인 흐름만을 파악할 수 있도록 했다.
const userData = await loginMutation({id: idText, password: passwordText})
if(userData){
setUserInfo(userData)
AsyncStorage.setItem('accessToken', userData.accessToken);
}
로그인 api response로 받아온 userData에는 access token이 포함되어 있다. 로그인이 성공해서 userData를 받아왔다면 이를 전역상태에 할당하여 전역에서 접근할 수 있도록 한다. 만약 apollo나 relay, react query와 같은 라이브러리를 사용한다면 전역상태 대신 api cache를 활용할 수도 있다.
또한 이와 병렬적으로 local storage에 access token을 저장해준다. 이 access token은 이후 앱을 init할 때 자동으로 로그인해주는데 사용된다. 참고로 react native 내장 local storage는 removed 되었기 때문에, local storage를 사용하기 위해서는 커뮤니티 라이브러리를 사용해야한다.
axios.interceptors.request.use(
request => {
request.headers.Authorization = useUserInfo.getState().userInfo.accessToken || 0; //axios와 zustand 사용 예시
return request;
}
);
그렇게 저장된 access token을 위와 같이 모든 api에 자동으로 넣어주도록 조치해두면 편리하다.
//App.tsx
...
useEffect(()=>{
const accessToken = await AsyncStorage.getItem('accessToken')
if(accessToken){
const userData = await getUserInfoQuery(accessToken)
if(userData){
setUserInfo(userData)
}
}
...
},[])
앱 실행 시점에 최상위에서 local storage를 체크해서 access token 여부를 확인하여 자동으로 로그인시키는 작업을 수행한다. access token으로 user info를 받아오고 그 결과를 setting하는 방식으로 구현했지만 전역 상태를 userInfo에서 accessToken을 별도로 분리한 뒤, 벙렬적으로 진행해도 무방하다.
const signOut = () =>{
AsyncStorage.removeItem('accessToken');
setUserInfo(null)
}
당연히 로그아웃할 때는 local storage도 함께 지워주어야한다.