프로젝트에 주어진 시간은 단 6주.
이런 초단기 프로젝트에서 가장 큰 리스크는 예측 불가능한 버그로 인한 개발 지연입니다. 이전 프로젝트에서는 사용자 인증 처리로 인해 QA 단계에서 불필요한 시간을 많이 소요했기에, 우선적으로 인증 처리 부분을 기획전 잡아가고자 했습니다.
어떻게 하면 인증 관련 이슈를 원천 차단하고,
비즈니스 로직에만 집중할 수 있는 환경을 만들 수 있을까?
선제적 아키텍처 설계와 자동화
저희는 문제가 터진 뒤에 해결하는 대신, 개발 시작 단계에서부터 견고한 인증 아키텍처를 설계했습니다. 핵심 전략은 '자동화'와 '보안'이었습니다.
1. 보안 원칙 수립: 토큰 분리
React Native와 WebView를 함께 사용하는 하이브리드 환경이었기에, 보안을 최우선으로 고려했습니다.
- RefreshToken은 네이티브(RN)에만 안전하게 보관하여 탈취 위험을 원천 차단했습니다.
- 수명이 짧은 AccessToken만 WebView(React)로 전달하여 만에 하나 토큰이 유출되더라도 피해를 최소화했습니다.
2. 구현: Axios Interceptor로 모든 과정 자동화
모든 API 통신에 사용하는 Axios에 Interceptor를 적용하여 인증 과정을 자동화했습니다.
1) 모든 요청에 토큰 자동 주입 (Request Interceptor)
API 요청이 보내지기 전에 Interceptor가 이를 가로채, WebView Bridge 또는 localStorage에서 토큰을 가져와 Authorization 헤더에 자동으로 추가합니다. 개발자는 더 이상 헤더를 신경 쓸 필요가 없습니다.
apiInstance.interceptors.request.use(async (config) => {
// WebView Bridge 또는 LocalStorage에서 토큰 가져오기
const accessToken = await getAccessToken();
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
});
2) 토큰 만료 시 자동 갱신 (Response Interceptor)
API 응답이 401 Unauthorized 에러일 경우, Interceptor가 이를 감지하고 토큰 갱신 프로세스를 자동으로 실행합니다.
이 과정은 다음과 같이 동작합니다.

- 401 에러 감지 시, 토큰 갱신 요청을 단 한 번만 실행합니다. (flag 변수 사용)
- 갱신이 진행되는 동안 실패한 다른 API 요청들은 잠시 '큐(Queue)'에 보관합니다.
- 네이티브(RN)에 토큰 갱신을 요청하여 새로운 AccessToken을 받아옵니다.
- 갱신에 성공하면, 큐에 있던 모든 요청들을 새 토큰으로 재실행합니다.
let isRefreshing = false;
let failedQueue = [];
const processQueue = (error, token = null) => {
failedQueue.forEach(promise => {
if (error) {
promise.reject(error);
} else {
promise.resolve(token);
}
});
failedQueue = [];
};
apiInstance.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
// 갱신이 이미 진행 중이라면, 현재 요청을 큐에 추가
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then(token => {
originalRequest.headers['Authorization'] = 'Bearer ' + token;
return apiInstance(originalRequest);
})
.catch(err => {
return Promise.reject(err);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
// 네이티브에 토큰 갱신 요청 후 새 토큰 받기
const newAccessToken = await refreshAccessToken();
// 큐에 쌓여있던 모든 요청들을 성공 처리
processQueue(null, newAccessToken);
// 현재 실패했던 요청도 새 토큰으로 재시도
originalRequest.headers['Authorization'] = 'Bearer ' + newAccessToken;
return apiInstance(originalRequest);
} catch (refreshError) {
// 갱신 실패 시 큐에 쌓인 요청들도 실패 처리
processQueue(refreshError, null);
// 로그아웃 처리 등...
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
6주 내 안정적인 앱 출시 성공
이러한 선제적인 아키텍처 설계는 세 가지 명확한 결과로 이어졌습니다.
- 프로젝트 성공: 6주라는 짧은 기간 동안 인증 관련 이슈로 인한 지연 없이 프로젝트를 안정적으로 완료할 수 있었습니다.
- 개발 생산성 향상: 개발자들은 토큰의 존재 자체를 잊은 채 각자 맡은 비즈니스 로직 개발에만 온전히 집중할 수 있었습니다.
- 최상의 사용자 경험: 사용자는 토큰 만료로 인해 로그아웃되거나 서비스를 이용하는 데 끊김을 전혀 경험하지 않았습니다.
'개발' 카테고리의 다른 글
| React Native와 WebView, 타입스크립트로 안전하게 통신하기 (0) | 2025.09.13 |
|---|---|
| Ref를 이용해 전역 모달 시스템 구축하기 (0) | 2025.03.24 |
| TanStack의 QueryClient를 이용해 Cache 데이터 활용하기 (0) | 2025.03.23 |
| 재귀 호출을 통한 데이터 구조 변환 (0) | 2025.03.09 |