API 요청을 할 때, 401 Unauthorized 응답이 오면 보통 액세스 토큰을 갱신(Refresh)하는 로직을 추가한다.
이 과정에서 ky 라이브러리를 활용하면 beforeRetry 훅을 사용해 비교적 간결하게 구현할 수 있다.
일단 ky에서 제공하는 BeforeRetryHook을 살펴보면
BeforeRetryHook에는 다음과 같은 타입이 정의되어 있다.
👉 [ky 공식 문서](https://www.npmjs.com/package/ky#:~:text=users%27)%3B-,hooks.beforeRetry,-Type%3A%20Function)
ky
Tiny and elegant HTTP client based on the Fetch API. Latest version: 1.7.5, last published: 6 days ago. Start using ky in your project by running `npm i ky`. There are 698 other projects in the npm registry using ky.
www.npmjs.com
여기에서 제공하는 retryCount를 사용하냐 마냐가 오늘의 주제이다.
참고로 retryCount는 ky에서 자동으로 0부터 시작해 증가하는 값이다.
[React] ky로 로그인 유지 구현하기 (with. JWT 인증)
들어가며 ky는 fetch와 ESM을 기반으로 하는 HTTP Client를 제공하는 라이브러리입니다. 주로 사용되는 Axios의 interceptors와 비슷하게 ky는 응답을 intercept 하여 커스텀하게 조작이 가능한 AfterResponse, Befor
devpluto.tistory.com
처음에는 이 글을 참고해서 비슷하게 구현하려고 했고 내가 이해한 글의 코드처럼 마지막 재시도 전에 로그아웃을 수행하도록 구현했다.
const DEFAULT_API_RETRY_LIMIT = 2;
const handleTokenRefresh: BeforeRetryHook = async ({ error, retryCount }) => {
// retryCount가 limit-1일 때(마지막 재시도 전) 로그아웃
if (retryCount === DEFAULT_API_RETRY_LIMIT - 1) {
authService.logout();
return ky.stop;
}
그러나... 모각코 시간에 이게 무슨 로직이냐는 질문에 난 대답을 할 수 없었고 ㅎ
retry.limit으로 이미 재시도 횟수를 제한하고 있는데, retryCount 체크가 필요할까?
코드처럼 마지막 재시도 전에 로그아웃하는 것이 좋은 방법일까?
라는 고민을 해보고자 한다.
고민을 하고 코드를 짰어야 했는데 ㅎ
ㅎ
ㅎ
두 가지 접근 방식 비교
일단 기존 코드였던 (retryCount === DEFAULT_API_RETRY_LIMIT - 1) 이렇게 마지막 재시도 전을 체크하는 방식을 알아보자.
✅ 첫 번째 방식 : retryCount를 체크해 마지막 재시도 전을 감지
const handleTokenRefresh: BeforeRetryHook = async ({ error, retryCount }) => {
const httpError = error as HTTPError;
if (httpError.response.status !== 401) {
return ky.stop;
}
if (retryCount === DEFAULT_API_RETRY_LIMIT - 1) {
authService.logout();
return ky.stop;
}
const refreshToken = tokenService.getRefreshToken();
if (!refreshToken) {
authService.logout();
return ky.stop;
}
try {
await authService.refreshToken(refreshToken);
} catch (error) {
console.error("Token refresh 실패, 로그아웃", error);
authService.logout();
return ky.stop;
}
};
이 방식의 특징
- retryCount === DEFAULT_API_RETRY_LIMIT - 1 체크를 통해 마지막 재시도 전에 로그아웃을 수행한다.
- 마지막 요청을 수행하지 않고 미리 로그아웃하여 불필요한 요청을 방지한다.
이 방식의 흐름은 다음과 같다.
1️⃣ 첫 번째 API 요청 실패 (`401` 에러) → `retryCount = 0`
2️⃣ `refreshToken`으로 액세스 토큰 갱신 시도
3️⃣ 두 번째 시도 직전(`retryCount = 1`)
- `retryCount === DEFAULT_API_RETRY_LIMIT - 1 (1 === 2-1)`이므로 조건이 참이 되고 로그아웃 실행
- 마지막 요청을 보내지 않고 중단
마지막 시도를 생략하고 미리 로그아웃하는 것이 이 방식의 핵심이다.
결론적으로 DEFAULT_API_RETRY_LIMIT를 쓰는 이유는
=> 더 시도해봤자 실패할 거 같으니까 미리 포기하고 로그아웃 한다는 의미~ 라고 이해하면 되겠다.
주의할 점
ky.limit을 쓰지 않고 일부러 retryCount를 체크하는 의도는 이해했다.
마지막 시도 전을 감지해서 사용자 경험을 더 좋게 하려는 건데
limit 횟수가 3-4번도 아니고 2번일 거면 한 번만 시도하고 로그아웃하나 마지막 재시도 전을 감지해서 중지하나 똑같으니 최소 횟수를 4 이상으로 설정하자.
내 코드처럼 DEFAULT_API_RETRY_LIMIT가 2인 이상 retryCount를 체크하는 방식이 불필요하게 복잡함 대신
DEFAULT_API_RETRY_LIMIT = 4이면 3번까지는 시도해보고, 4번째 요청은 막으니까 이런 로직에 좋음
장점
refreshToken이 만료된 경우, 마지막 재시도를 하지 않고 즉시 로그아웃하니까
세션이 만료되었음을 빠르게 반영 → 사용자 경험(UX) 개선이 된다.
✅ 두 번째 방식: ky의 기본 동작을 따르는 방식
import ky, { type BeforeRetryHook, HTTPError } from "ky";
import { authService } from "./auth.service";
import { tokenService } from "./token.service";
const DEFAULT_API_RETRY_LIMIT = 2;
const handleTokenRefresh: BeforeRetryHook = async ({ error }) => {
const httpError = error as HTTPError;
if (httpError.response.status !== 401) {
return ky.stop;
}
const refreshToken = tokenService.getRefreshToken();
if (!refreshToken) {
authService.logout();
return ky.stop;
}
try {
await authService.refreshToken(refreshToken);
} catch (error) {
console.error("Token refresh 실패, 로그아웃", error);
authService.logout();
return ky.stop;
}
};
이 방식의 특징
- retryCount를 따로 체크하지 않음 → ky의 기본 재시도 로직을 따름
장점
- ky의 기본적인 retry.limit 설정을 활용하여 불필요한 로직이 없음
단점
- refreshToken이 만료된 경우, 마지막 재시도를 수행한 후에야 로그아웃
- 즉, 마지막 요청이 의미 없이 실행될 가능성이 있음
이런 단점 때문에 처음 방식 (DEFAULT_API_RETRY_LIMIT을 체크하는 방식)을 고려하지 않았을까 싶다!
그래서 결론은 : 마지막 재시도 전에 로그아웃을 하는 것이 나을까?
일반적인 경우에는 ky의 기본적인 retry.limit 만 체크하는 것이 더 깔끔하고 직관적이다.
`retryCount`를 체크하지 않아도 `ky.retry.limit`이 이미 요청 횟수를 관리하기도 하니까. (물론 재시도 전을 감지하냐 마냐 차이가 있긴 하지만)
하지만 UX적으로 즉각적인 로그아웃이 필요하다면 앞서 말한 첫 번째 방식도 고려할 수 있다.
느낀점
ky 관련 예제나 코드가 많이 없어서 코드를 복붙해서 썼는데 어떤 로직인지 제대로 파악하지 않은채 쓴 건 실수였다.
사실 자료 없는 것도 한국어가 많이 없는 거지 문서 번역해서 보면 되긴 하는데... 귀찮아 한건 잘못된 행동 ㅎ
그래도 모각코 시간에 피드백을 받으면서 다시 고민해볼 기회가 생겼고 덕분에 어떤 방식이 더 나은지를 이해할 수 있었다!