[capacitor] 인증 로직 수정하기

2025. 12. 31. 15:37·기능/앱
반응형

지난 글에서는 Capacitor 커뮤니티 플러그인을 통해서 Android의 구글, 카카오, 그리고 애플 로그인을 진행했습니다. 이번 글에서는 기존에 쿠키로 사용자를 인증하는 방식에서 Bearer Header을 통한 인증 구현 방식에 대해서 알아보도록 하겠습니다. 

 

기존 백엔드 요청

기존에는 React Query를 기반으로 useGet과 usePost 커스텀 훅을 만들어 사용했었습니다. 

/**
 * React Query 기반 GET 요청 훅
 *
 * 자동으로 401 응답 시 refreshToken()을 호출하고,
 * 재시도까지 해주는 fetch 래퍼입니다.
 *
 * @template T 응답 데이터의 타입
 *
 * @param url API endpoint
 * @param key React Query의 queryKey (배열 형태, 직렬화 가능한 값만 가능)
 * @param enabled 쿼리 실행 여부 (기본값: true)
 * @param refresh_url refresh token 갱신 필요한 경우 API endpoint (기본값: "api/auth/refresh_token")
 * @param fallback refresh token 갱신 실패 시 돌아갈 url
 * @example
 * const { data, isLoading, error } = useGet<User[]>("/users", ["users"]);
 */
export const useGet = <T>(
  url: string,
  key: (string | number)[],
  enabled: boolean = true,
  refresh_url: string = "api/auth/refresh_token",
  fallback: string = "/",
) => {
  const refreshToken = useRefreshToken(refresh_url);

  return useQuery<T>({
    queryKey: key,
    enabled,
    queryFn: async () => {
      const makeRequest = async () => {
        return await fetch(`${baseURL}/${url}`, {
          credentials: "include",
        });
      };

      let response = await makeRequest();

      if (response.status === 401) {
        const ok = await refreshToken();
        if (!ok) {
          window.location.href = fallback;
          return;
        }
        response = await makeRequest();
      }

      if (!response.ok) {
        const text = await response.text();
        throw new Error(`API 오류 ${response.status}: ${text}`);
      }
      const data = await response.json();
      return data;
    },
    placeholderData: keepPreviousData,
  });
};
/**
 * React Query 기반 POST 요청 훅
 *
 * 자동으로 401 응답 시 refreshToken()을 호출하고,
 * 재시도까지 해주는 fetch 래퍼입니다.
 *
 * @template TResponse 응답 데이터 타입
 * @template TRequest 요청 바디 타입 (object, FormData, void)
 * @template TError 에러 타입 (기본: { status?: number; message?: string })
 *
 * @param url API endpoint
 * @param fallback refresh token 갱신 실패 시 돌아갈 url
 * @param refresh_url refresh token 갱신 필요한 경우 API endpoint (기본값: "api/auth/refresh_token")
 *
 * @example
 * // JSON Body 요청
 * const createUser = usePost<UserResponse, { name: string; email: string }>("/users");
 * createUser.mutate({ name: "홍길동", email: "hong@example.com" });
 *
 * @example
 * // FormData 요청 (파일 업로드)
 * const uploadFile = usePost<{ url: string }, FormData>("/upload");
 * const fd = new FormData();
 * fd.append("file", fileInput.files[0]);
 * uploadFile.mutate(fd);
 *
 * @example
 * // 바디 없는 POST (예: 로그아웃)
 * const logout = usePost<void, void>("/auth/logout");
 * logout.mutate();
 */
export const usePost = <
  TRequest extends object | FormData | void,
  TResponse,
  TError = { status?: number; message?: string },
>(
  url: string,
  fallback: string = "/",
  refresh_url: string = "api/auth/refresh_token",
) => {
  const refreshToken = useRefreshToken(refresh_url);

  return useMutation<TResponse, TError, TRequest>({
    mutationFn: async (body: TRequest) => {
      // 헤더 조건부
      const headers: HeadersInit = {};
      let fetchBody: BodyInit;

      if (body instanceof FormData) {
        fetchBody = body;
        // FormData면 Content-Type 자동 설정 (headers에 아무것도 안 넣음)
      } else {
        fetchBody = JSON.stringify(body);
        headers["Content-Type"] = "application/json";
      }

      const makeRequest = async () => {
        return await fetch(`${baseURL}/${url}`, {
          method: "POST",
          headers,
          credentials: "include",
          body: fetchBody,
        });
      };

      let response = await makeRequest();

      if (response.status === 401) {
        const ok = await refreshToken();
        if (!ok) {
          window.location.href = fallback;
          throw new Error("세션 만료");
        }
        response = await makeRequest();
      }

      if (!response.ok) {
        const errorText = await response.text();
        let errorData;
        try {
          errorData = JSON.parse(errorText);
        } catch {
          errorData = null;
        }
        throw {
          status: response.status,
          message: errorData?.message || errorText || "Something went wrong",
        } as TError;
      }

      return (await response.json()) as TResponse;
    },
  });
};

사용 예시:

const { data: meData } = useGet<meDataProps>("api/client/me", ["me"], !!user);

 

기존 백엔드도 마찬가지로 로그인 이후 쿠키에 refresh token, access token, 그리고 user_info가 담긴 token을 넘겼기 때문에, 로그인이 필요한 로직에 접근 시 쿠키에서 유저 정보를 찾고, 이를 통해 인증을 진행했습니다.

 

사용 예시:

def with_login(func):
    """로그인 필수 라우트용 데코레이터"""
    @wraps(func)
    async def wrapper(p, *args, **kwargs):
        token_util = AuthToken()
        try:
            user_id = await token_util.get_token_info(p.request)
        except HTTPException:
            raise HTTPException(status_code=401, detail="Unauthorized")
        return await func(p, *args, **kwargs)
    return wrapper
async def get_token_info(self, request):
    return self._get_user_id_from_cookies(request.cookies)
def _get_user_id_from_cookies(self, cookies):
    try:
        user_info = cookies.get("user_info")
        access_token = cookies.get("access_token")
        if not user_info or not access_token:
            raise HTTPException(status_code=401, detail="invalid token data")

        decoded = base64.b64decode(user_info).decode("utf-8")
        data = json.loads(decoded)
        user_id = data.get("id")
        if not user_id:
            raise HTTPException(status_code=401, detail="invalid token data")

        return user_id
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=401, detail=str(e))

하지만 Capacitor를 사용하면 쿠키에 접근을 할 수 없으므로 Bearer Header를 보내고, 그 안에서 user_info를 파싱하는 코드를 추가해줍시다. 지난 글에서 만든 getAuth()를 통해 Preferences에 저장된 유저 정보를 알 수 있습니다. 헤더를 바로 만들어주는 hook을 하나 만들어 보죠.

// 네이티브에서만 bearer header 추가 
export const getAuthNativeHeaders = async () => {
  const headers = {};

  if (Capacitor.isNativePlatform()) {
    const auth = await getAuth();

    if (!auth?.user_info) return headers;
    headers["Authorization"] = `Bearer ${auth.user_info}`;
  }
  return headers;
};

그리고 각각 useGet, usePost에 추가해줍시다.

useGet 변경 부분: 

const makeRequest = async () => {
  const headers = await getAuthNativeHeaders();
  return await fetch(`${baseURL}/${url}`, {
    credentials: "include",
    headers,
  });
};

usePost 변경 부분: 

const headers: HeadersInit = await getAuthNativeHeaders();
let fetchBody: BodyInit;

if (body instanceof FormData) {
  fetchBody = body;
  // FormData면 Content-Type 자동 설정 (headers에 아무것도 안 넣음)
} else {
  fetchBody = JSON.stringify(body);
  headers["Content-Type"] = "application/json";
}

이제 백엔드를 수정해줄 차례네요. 기존의 get_token_info는 쿠키에서만 유저 정보를 파싱했습니다. 그 위에 header가 있는 경우에, header에서 user_info를 파싱하도록 분기처리 해줍시다.

async def get_token_info(self, request):
    token = None
    headers = request.headers
    try:
        token = headers.get("authorization").replace("Bearer ", "")
        if token :
            decoded = base64.b64decode(token + "==").decode('utf-8')
            payload = json.loads(decoded)
            return payload.get("id")
    except:
        return self._get_user_id_from_cookies(request.cookies)

이렇게 코드를 처리해주면, @with_login, 즉 로그인이 필요한 서비스에 대해서 웹과 네이티브 모두 각각 쿠기와 헤더에서 유저 정보를 불러올 수 있고, 없는 경우 401에러를 응답하게 됩니다. 

 

이번 글에서는 Capacitor 환경에서 쿠키를 사용할 수 없는 문제를 해결하기 위해, Bearer Header를 이용한 사용자 인증 방식으로 전환하는 과정을 살펴봤습니다. 웹에서는 기존 쿠키 기반 인증을 유지하면서, 네이티브 환경에서는 Authorization 헤더를 통해 동일한 인증 로직을 재사용할 수 있도록 구성함으로써 하나의 백엔드 코드로 두 환경을 대응할 수 있었습니다. 다음 글에서는 Capacitor 환경에서 카메라, 마이크, 알림 권한을 어떻게 설계하고 처리했는지, 실제 구현 과정과 함께 처리해보려고 합니다. 

 

언제나처럼 ㅡ 시작은 삽질이지만, 끝은 지식입니다.

 

 

반응형

'기능 > 앱' 카테고리의 다른 글

[capacitor] Android 소셜 로그인 적용하기  (0) 2025.12.31
[capacitor] Safe Area 적용하기  (0) 2025.12.31
[capacitor] 앱 패키징 시작하기  (1) 2025.12.30
[capacitor] 앱 패키징  (0) 2025.11.11
'기능/앱' 카테고리의 다른 글
  • [capacitor] Android 소셜 로그인 적용하기
  • [capacitor] Safe Area 적용하기
  • [capacitor] 앱 패키징 시작하기
  • [capacitor] 앱 패키징
그낙이
그낙이
시작은 삽질이지만, 끝은 지식입니다.
  • 그낙이
    개발 삽질 일지
    그낙이
  • 전체
    오늘
    어제
    • 분류 전체보기 (71)
      • 서버 (12)
        • 터미널 기본기 (4)
        • AWS (3)
        • Linux (5)
      • 아키텍처 (3)
      • 기능 (19)
        • 로그인 (4)
        • API (5)
        • 앱 (5)
        • 기타 (4)
      • 자유로운 개발일지 (37)
        • APP (4)
        • AI (7)
        • 직링 (19)
        • 자동매매 (6)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    콘서트
    티켓
    fiddler
    챗봇
    업비트
    Capacitor
    apple developer
    FastAPI
    예매
    IAP
    퍼피티어
    GPT
    직링
    소셜 로그인
    개발자 도구 우회
    코인
    자동화 도구
    개발자 도구
    인앱 결제
    linux
    nginx
    앱
    puppeteer
    챗봇 만들기
    EC2
    웹소켓
    apple connect store
    비트코인
    자동매매
    kotlin
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
그낙이
[capacitor] 인증 로직 수정하기
상단으로

티스토리툴바