오늘은 전부터 해결하려고 고민했던 ApolloClient로 AccessToken을 자동 갱신하는 기능을 구현했다. 전에 다른 에러를 검색하다, 에러 핸들링 함수를 통해서 자동으로 AccessToken을 갱신하는 방법에 대해서 본것 같았다. 그래서 그 부분을 검색해봤더니, 이미 문제를 해결해서 포스팅한 사람이 있었다.
https://chanyeong.com/blog/post/47
Apollo Client 토큰 재발급 :: chanyeong
클라이언트와 서버가 통신할 때 토큰을 사용할 경우 토큰을 탈취당할 우려가 있기 때문에 보통 API에 접근할 때 사용하는 Access Token과 Access Token을 재발급 받는 Refresh Token으로 구성하는 경우가 많
chanyeong.com
덕분에 아주 쉽게 구현을 했다. 그렇다고 아예 복붙한 것은 아니니, 내가 문제를 해결한 과정을 한번 살펴보자.
Apollo Client공식 문서- Error Handling
Handling operation errors
const { loading, error, data } = useQuery(MY_QUERY, { errorPolicy: "all" });
www.apollographql.com
Apollo Client의 문서에서 Error Handling부분을 보면, 에러가 발생했을때 다시 fetch동작을 수행하는법이 나와있다. 게다가 예시가 UNAUTHENTICATED이다. 우리가 찾던 사용자 인증과 관련된 부분이다.
onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (let err of graphQLErrors) {
switch (err.extensions.code) {
// Apollo Server sets code to UNAUTHENTICATED
// when an AuthenticationError is thrown in a resolver
case 'UNAUTHENTICATED':
// Modify the operation context with a new token
const oldHeaders = operation.getContext().headers;
operation.setContext({
headers: {
...oldHeaders,
authorization: getNewToken(),
},
});
// Retry the request, returning the new observable
return forward(operation);
}
}
}
// To retry on network errors, we recommend the RetryLink
// instead of the onError link. This just logs the error.
if (networkError) {
console.log(`[Network error]: ${networkError}`);
}
});
코드를 보면 onError라는 함수의 인자에 콜백 함수를 만들어서 넣어주고있다. if조건문으로 graphQLErrors가 있다면, 반복문을 거쳐서 UNAUTHENTICATED인 에러 코드를 찾는다. 만약 UNAUTHENTICATED에러가 있다면, 헤더의 토큰을 수정해서 다시 요청을 보내는 내용이다.
문서에서는 이렇게 에러코드의 내용으로 구별해서, 다시 요청을 보낼수 있다고 적혀있다. 그리고 기타 다른 네트워크 에러는 RetryLink를 통해 처리하게 되어있다.
우리가 토큰이 만료된 채로 요청을 보내면, 서버쪽에서 UNAUTHENTICATED코드를 보내게 되고, 우리는 그 코드를 확인해서 토큰을 재발급 받고난뒤, 헤더를 수정해서 다시 서버로 이전의 요청을 보내게 되면 정상적으로 응답을 받을수 있게 된다.
그러면 에러코드 구별하는부분까지는 나와있으니, 토큰 재발급 받는 부분을 작성해보자. 이제 내가 작성한 코드가 나온다.
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if(client && graphQLErrors?.filter(error=>error.message === 'Unauthorized').length){
// 토큰 재발급을 위해 accessToken과 refreshToken을 준비.
const accessToken = getAccessToken();
const refreshToken = getRefreshToken();
// 토큰이 두개중 하나라도 없으면 함수 탈출
if(!(accessToken && refreshToken)){
return ;
}
//fromPromise함수에 토큰 재발급 mutation 넣어서 return받음.
const promises = fromPromise(
// client.mutate함수로 useMutation동작을 수행한다.
// 토큰을 재발급 받는 API작업을 수행한다.
client.mutate<MReissueAccessToken, MReissueAccessTokenVariables>(
{mutation:REISSUE_ACCESS_TOKEN, variables:{args:{accessToken, refreshToken}}})
.then(data=> {
// 응답이 정상적으로 왔다면 이어서 진행.
// accessToken이 담겨왔다면, localStorage에 저장.
if(data.data?.reissueAccessToken.accessToken){
setAccessToken(data.data.reissueAccessToken.accessToken);
}
// refreshToken이 만료되었다면, 캐시를 전부 지우고, 로그인 해제
if(data.data?.reissueAccessToken.isRefreshTokenExpired){
if(()=>getAccessToken()){
client.resetStore()
loginState(false)
}
}
})
)
//mutation에 flatMap으로 promise된 요청이 있었다면 헤더를 바꿔서 이전요청 다시 요청
return promises.flatMap(()=>{
// 이전의 요청에서 header불러오기
const oldHeaders = operation.getContext().headers;
operation.setContext({
headers: {
...oldHeaders,
// localStorage에 저장했던 accessToken다시 호출
authorization: `Bearer ${getAccessToken()}`,
},
})
return forward(operation)
})
}
//이 이외에 다른 에러가 있다면 console.log
console.log(
`[GraphQL error]: Message: ${graphQLErrors?.[0].message}, Location: ${graphQLErrors?.[0].locations}, Path: ${graphQLErrors?.[0].path}`
)
if (networkError) console.log(`[Network error]: ${networkError}`);
});
graphQLErrors : graphQL엔드포인트로부터 응답된 에러내용
networkError: 네트워크오류. 서버와 연결이 안될때 전달됨.
operation: 에러가 발생한 요청
forward:이 함수에 인자를 담으면 다음 Apollo Link로 전달됨.
토큰 재발급을 위한 Mutation을 fromPromise함수를 통해서 promise함수를 담아서 사용한다. 이걸 하면서 제일 오래걸렸다. onError함수를 포함한 Apollo Link함수들은 promise를 return할 수 없다. 함수의 타입이 그렇게 정해져 있기때문이다. 해당내용이 깃허브에 이슈가 되어있다. 여기에 있는 코드들을 참고해서 문제를 해결했다.
https://github.com/apollographql/apollo-link/issues/646
apollo-link-error - how to async refresh a token? · Issue #646 · apollographql/apollo-link
Issue Labels has-reproduction feature docs blocking <----- sorry, not blocking good first issue Question The precise scenario I'm trying to accomplish is mentioned here: https://github.com/a...
github.com
단순히 async/await을 반환하는게 아닌, 응답을 캐싱해야하고, 해당 함수를 호출한 컴포넌트에 state가 변했다는 내용도 전달해야 해서 이렇게 하는건가? 라는 생각이 든다. 관련내용은 나중에 더 공부를 해봐야겠다.
만들어진 onError함수는 ApolliClient인스턴스를 만들때 넣어주면 된다. 여러개의 link를 사용하는경우, concat함수를 적절히 사용하자.
export const client = new ApolloClient({
cache,
link: from([authMiddleware, errorLink.concat(httpLink)]),
});
accessToken을 3초로하고, refreshToken을 30초로 한다음, 새로고침을 하면서 확인해본결과, 3초에한번 토큰 재발급 요청을 하고, 30초 후 에는 로그아웃이 진행되었다. 정상적으로 동작을 하는걸 확인했다.
어플리케이션 환경에서 해당 기능을 직접 구현해봤는데, 그전에 구현한것보다 훨씬 코드가 깔끔해진 느낌이다. 그전엔 토큰이 만료되기전에 먼저 요청을 하거나, 만료된걸 확인하고 다시 요청하는 방식이었는데, 이 방법보다 훨씬 직관적인것 같다.
'소셜독' 카테고리의 다른 글
산책기록 gps 필터링 개선 (0) | 2022.06.13 |
---|---|
산책 기록 기능 Geolocation + Foreground Tasks (0) | 2022.06.13 |
bcrypt로 비밀번호 암호화 (0) | 2022.06.12 |
React + Apollo Client-Side Pagination Cache (0) | 2022.06.12 |
Server-side Pagination (0) | 2022.06.12 |