REST API

2025. 8. 6. 22:39·기능/API
반응형

이번 글에서는 REST API에 대해서 이야기 해보겠습니다. 웹 개발에서 데이터를 다루는 기본 동작을 CRUD라고 부릅니다. 클라이언트와 서버가 API를 통해 데이터를 주고 받는 과정이죠. 새로운 데이터를 만드는 CREATE, 데이터를 읽어오는 READ, 데이터를 수정하는 UPDATE, 그리고 데이터를 삭제하는 DELETE 입니다.

 

데이터를 가져오고, 만드는 걸 가장 많이 사용하는데, 이는 HTTP 메서드의 GET과 POST에 해당합니다. 간단하게, GET은 데이터를 조회할 때 쓰고, POST는 데이터를 생성하거나 서버에 어떤 작업을 요청할 때 씁니다. 비밀번호 변경처럼 “조회”가 아닌 상태 변경 작업은 수정이더라도 POST로 처리하는 게 일반적입니다. 즉, POST는 단순 생성뿐 아니라 서버 상태를 바꾸는 모든 요청에 사용할 수 있습니다.

 

HTTP 요청을 보내는 코드와 방식은 개발자마다 천차만별일 수 있습니다. 데이터를 가져오는 가장 기본적인 방식은 fetch 또는 axios입니다. 코드로는 아래처럼 사용할 수 있겠네요. 

const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
  axios.get('/api/users')
    .then(res => setUsers(res.data))
    .finally(() => setLoading(false));
}, []);

 

이 방법도 잘 작동하지만, 로딩 상태 관리, 에러 처리, 캐싱, 자동 재요청 등등 불편함이 있습니다. 이를 해결하기 위해 SWR, RTK Query 등 다양한 라이브러리가 존재하지만 저한테 제일 익숙하고 다운로드 수가 많은 Tanstack Query(구 React Query)로 HTTP 요청을 보내는 함수를 컴포넌트화 해보려고 합니다.(이용자가 많다는건 소스 코드도 많고 생태계가 크다는 뜻이니까요, 업데이트도 잘 이루어질거라 생각합니다)

 

REST API

그 전에 REST API에 다루고 넘어가보죠. REST API란 화면과 서버가 나누는 대화의 약속입니다. 다들 익숙하실 API는 Application Programming Interface의 약자로 쉽게 생각하시면 대화 방식의 정의라고 생각하시면 됩니다. 앞에 붙는 REST는 Representational State Transfer로, 웹에서 API를 깔끔하게 쓰는 약속입니다. 

 

REST의 핵심 원칙은 크게 3가지입니다. 

1. 리소스 중심 : 무엇을 다루는 지가 중요합니다. 예를 들어 콘서트라면   /concerts , 티켓이라면  /tickets 가 되겠네요.

2. 주소와 동작 구분 : 주소는 명사, 동사는 HTTP 메서드로 표현합니다. GET /concerts는 콘서트 조회하기가 됩니다.

3. 무상태 : 서버는 이전 대화를 기억하지 않습니다. 필요한 모든 정보를 같이 보내줘야합니다.  

 

Tanstack Query

다음은 탠스택 쿼리(리액트 쿼리)에 대한 설명입니다. 자세한 내용은 아래 공식문서에서 확인하실 수 있습니다. useInfiniteQuery, useQueries, prefetchQuery 등 다양한 기능들이 있으니 공부해보시는 것도 추천드립니다.

 

Overview | TanStack Query React Docs

TanStack Query (formerly known as React Query) is often described as the missing data-fetching library for web applications, but in more technical terms, it makes fetching, caching, synchronizing and...

tanstack.com

 

탠스택 쿼리를 사용하기 전에 최상단에서 QueryClientProvider로 감싸주어야합니다. 이제 각각의 컴포넌트에서 탠스택 쿼리를 사용할 준비가 끝났습니다.

import {
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'

const queryClient = new QueryClient()

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  )
}

 

먼저 GET 요청입니다. useQuery는 데이터를 읽는 GET 요청에 사용됩니다. 여기서 사용되는 게 Query Key입니다. Query Key는 캐시 식별자의 역할로, 동일한 API라고 할지라도 파라미터가 달라지면 다른 Query Key를 부여하는게 바람직합니다. 즉 데이터 무효화, 재조회 시 대상 쿼리를 지정하는 기준이 됩니다. 예시 코드는 아래와 같습니다. 

import { useQuery } from '@tanstack/react-query'

const { data, isLoading, error } = useQuery(queryKey, queryFn, options)

 

각 인자에 대해 살펴봅시다. 아까 말한 Query Key입니다. 문자열 또는 배열로 지정할 수 있으며 단순한 데이터에서는 queryKey를 문자열로 사용하면 간단하게 관리할 수 있고, 복잡한 데이터에서는 [”todos”, {category: “tech”}] 처럼 조건에 따라 캐시를 구분할 수 있습니다.

// 문자열
useQuery('todos', ...)
// 배열
useQuery(['todos'], ...)

 

두번째 인자인 Query Function입니다. 

const fetchPosts = async () => {
  const res = await fetch(`${baseUrl}/post/all`);
  if (!res.ok) {
    throw new Error("데이터를 불러오는 데 실패했습니다.");
  }
  return res.json();
}

const { data, isLoading, error } = useQuery({
  queryKey: "postData", 
  queryFn: fetchPosts
});

if (isLoading) {
  return <div>로딩 중...</div>; 
}

if (error) {
  return <div>에러가 발생했습니다</div>; 
}

 

이를 이용한 커스텀 훅인 useGet입니다. 이전 카카오, 구글 로그인에서 캐시에 사용자 정보를 저장했기 때문에 credentials include를 통해 캐시를 보내줍니다. 또한 401에러가 나온 경우 refresh token을 요청하고, 성공하면 재요청합니다. placeholderData: keepPreviousData는 페이지네이션 시 이전 페이지 데이터를 잠깐 유지해주는 React Query 옵션으로 페이지네이션이 필요한 경우가 있어서 추가해줬습니다. 

export const useGet = <T>(
  url: string,
  key: (string | number)[],
  enabled: boolean = true,
) => {
  const refreshToken = useRefreshToken();

  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) {
        await refreshToken();
        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,
  });
};

 

사용법입니다. 

  const { data: totalUserData, isLoading, error } = useGet<UserResponse>(
    `api/admin/users?company_id=${companyId}&page=${page}&search_term=${searchTerm}&search_type=${searchType}`,
    ["users", searchType, searchTerm, companyId, page],
  );

 

회사 ID를 통해서 사용자들을 조회해오고, 검색 방식, 검색어, 회사 ID, 페이지네이션에서 페이지가 바뀔 때마다 자동으로 refetch 해줍니다. 사용자 삭제, 수정 등을 통해 POST를 한 경우 아래와 같이 데이터를 재요청을 보낼 수 있습니다. 컴포넌트 렌더링 시 자동으로 실행되기 때문에 useEffect 처리는 불필요합니다.

queryClient.invalidateQueries({ queryKey: ["users"] });

 

이제 POST 요청입니다. useMutation은 POST, DELETE, 그리고 PUT까지 여러 기능을 한번에 담당해줍니다. useQuery와는 다르게 캐시를 유지하지 않고 mutate를 호출해야 실행합니다.

const mutation = useMutation(async (roomId:number) => {
  const response = await fetch(`/room/${roomId}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ data }),
  });

  return response.json();
});
const handleEnterRoom = async (roomNumber: number) => {
  await mutation.mutateAsync(roomNumber);
};

 

아래는 커스텀 훅인 usePost입니다. useGet과 마찬가지로 401시 refresh token 요청이 포함되어 있습니다. useMutation의 경우 파일 데이터를 업로드할 때는 header가 없어야 하기 때문에 추가로 분기처리 해두었습니다. 

export const usePost = <
  TResponse,
  TRequest extends object | FormData,
  TError = { status?: number; message?: string },
>(
  url: string,
) => {
  const refreshToken = useRefreshToken();

  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) {
        await refreshToken(); 
        response = await makeRequest(); 
      }

      if (!response.ok) {
        const errorData = await response.json().catch(() => null);
        throw {
          status: response.status,
          message: errorData?.message || "Something went wrong",
          field: errorData?.field,
        } as TError;
      }

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

 

사용법입니다. 위에서 언급한 바와 같이 mutate를 통해서 useMutation을 호출해줍니다. 또한 invalidateQueries를 통해 캐시를 날리고, users에 대한 재요청을 보낸 것을 확인할 수 있습니다. 

const deleteUserMutation = usePost<
  DeleteUserResponse,
  DeleteUserRequest,
  DeleteUserError
>("api/admin/delete_user");

const handleDeleteUser = () => {
  deleteUserMutation.mutate(
    { id: targetUser.id },
    {
      onSuccess: () => {
        setIsModal(false);
        setIsToastMessage(true);
        queryClient.invalidateQueries({
          queryKey: ["users", searchType, searchTerm, companyId],
        });
      },
    },
  );
};

 

지금까지 REST API의 기본 개념부터 TanStack Query를 활용해 GET과 POST 요청을 컴포넌트화하는 방법까지 알아봤습니다.  
이 구조를 잘 잡아두면 프로젝트 전반의 API 호출 코드가 훨씬 간결해지고 유지보수가 쉬워집니다. 다음 글에서는 카카오와 구글 로그인과 함께 useGet, usePost를 배포하여 다른 프로젝트에서 불러서 사용해보도록 하겠습니다. 

 

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

반응형

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

[애플] 인앱 결제 - 구독 상품 API 2  (0) 2026.01.22
[애플] 인앱 결제 - 구독 상품 API  (0) 2026.01.21
[애플] 인앱 결제 API 2  (0) 2026.01.17
[애플] 인앱 결제 API  (0) 2026.01.17
'기능/API' 카테고리의 다른 글
  • [애플] 인앱 결제 - 구독 상품 API 2
  • [애플] 인앱 결제 - 구독 상품 API
  • [애플] 인앱 결제 API 2
  • [애플] 인앱 결제 API
그낙이
그낙이
시작은 삽질이지만, 끝은 지식입니다.
  • 그낙이
    개발 삽질 일지
    그낙이
  • 전체
    오늘
    어제
    • 분류 전체보기 (71)
      • 서버 (12)
        • 터미널 기본기 (4)
        • AWS (3)
        • Linux (5)
      • 아키텍처 (3)
      • 기능 (19)
        • 로그인 (4)
        • API (5)
        • 앱 (5)
        • 기타 (4)
      • 자유로운 개발일지 (37)
        • APP (4)
        • AI (7)
        • 직링 (19)
        • 자동매매 (6)
  • 블로그 메뉴

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

  • 공지사항

  • 인기 글

  • 태그

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

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
그낙이
REST API
상단으로

티스토리툴바