From 4509be24d8416751a622b74160ca5d34fc08e467 Mon Sep 17 00:00:00 2001 From: doitchuu Date: Tue, 6 May 2025 17:14:55 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[Docs]=207.2=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../seulgi.md" | 125 +++++++++++++++++- 1 file changed, 124 insertions(+), 1 deletion(-) diff --git "a/CH07_\353\271\204\353\217\231\352\270\260_\355\230\270\354\266\234/7.2_API_\354\203\201\355\203\234_\352\264\200\353\246\254\355\225\230\352\270\260/seulgi.md" "b/CH07_\353\271\204\353\217\231\352\270\260_\355\230\270\354\266\234/7.2_API_\354\203\201\355\203\234_\352\264\200\353\246\254\355\225\230\352\270\260/seulgi.md" index 3beced8..dfe79c8 100644 --- "a/CH07_\353\271\204\353\217\231\352\270\260_\355\230\270\354\266\234/7.2_API_\354\203\201\355\203\234_\352\264\200\353\246\254\355\225\230\352\270\260/seulgi.md" +++ "b/CH07_\353\271\204\353\217\231\352\270\260_\355\230\270\354\266\234/7.2_API_\354\203\201\355\203\234_\352\264\200\353\246\254\355\225\230\352\270\260/seulgi.md" @@ -1 +1,124 @@ - +# API 상태 관리하기 + +실제 API를 요청하는 코드는 컴포넌트 내에서 비동기 함수를 직접 호출하지 않는다. + +비동기 API를 호출하기 위해서는 API의 성공 실패에 따른 상태가 관리 되어야 하므로
+상태 관리 라이브러리의 액션이나 훅과 같이 재정의된 형태를 사용해야 한다. + +## 1) 상태 관리 라이브러리에서 호출하기 + +상태 관리 라이브러리의 비동기 함수들은 서비스 코드를 사용해서
+비동기 상태를 변화시킬 수 있는 함수를 제공한다. + +컴포넌트는 이런 함수를 사용해 상태를 구독하며, 상태가 변경될 때
+컴포넌트를 다시 렌더링하는 방식으로 동작한다. + +### Redux의 경우 + +비동기 상태가 아닌 전역 상태를 위해 만들어진 라이브러리이기 때문에
+미들웨어라고 불리는 여러 도구를 도입해 비동기 상태를 관리한다. + +보일러플레이트 코드가 많아지는 등 간편하게 비동기 상태를
+관리하기 어려운 상황도 발생한다. + +### MobX 같은 라이브러리의 경우 + +이런 불편함을 개선하기 위해 비동기 콜백 함수를 분리하여
+액션으로 만들거나 runInAction과 같은 메소드를 사용하여 상태 변경을 처리한다. + +async / await나 flow 같은 비동기 상태 관리를 위한 기능도 있어
+간편하게 사용 가능하다. + +```ts +// stores/userStore.ts +import { makeAutoObservable, runInAction } from "mobx"; + +class UserStore { + user: any = null; + loading = false; + + constructor() { + makeAutoObservable(this); + } + + async fetchUser() { + this.loading = true; + + try { + const response = await fetch("/api/user"); + const data = await response.json(); + + runInAction(() => { + this.user = data; + this.loading = false; + }); + } catch (error) { + runInAction(() => { + this.loading = false; + }); + + console.error(error); + } + } +} + +export const userStore = new UserStore(); +``` + +> 모든 상태 관리 라이브러리에서 비동기 처리 함수를 호출하기 위해 액션을 추가할 때마다
+> 관련된 스토어나 상태가 계속 늘어난다. + +> 가장 큰 문제점은 전역 상태 관리자가 모든 비동기 상태에 접근하고 변경할 수 있다는 것이다.
+> (2개 이상 컴포넌트가 구독하고 있는 비동기 상태는 쓸데없는 비동기 통신과 의도치 않은 상태 변경이 발생할 수 있음) + +
+
+ +## 2) 훅으로 호출하기 + +react-query나 useSwr과 같은 훅은 캐시를 사용해 비동기 함수를 호출하며,
+상태 관리 라이브러에서 발생했던 의도치 않은 상태 변경을 방지하는 데 도움이 된다. + +예를 들어, react query에서는 onSuccess 옵션의 invalidateQueries를 사용해
+특정 키의 API를 유효하ㅣ지 않은 상태로 설정할 수 있다. + +```ts +// hooks/usePosts.ts +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { createPost, fetchPosts } from "../api/posts"; + +export const usePosts = () => { + return useQuery({ queryKey: ["posts"], queryFn: fetchPosts }); +}; + +export const useCreatePost = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: createPost, + onSuccess: () => { + // 성공 시, posts 쿼리를 무효화해서 자동으로 refetch + queryClient.invalidateQueries({ queryKey: ["posts"] }); + }, + }); +}; +``` + +> 다만, 이후 컴포넌트가 반드시 최신 상태를 표현하려면 폴링이나 웹소켓 등의 방법을 써야한다. + +> [!NOTE] > **폴링(polling)?**
+> 클라이언트가 주기적으로 서버에 요청을 보내 데이터를 업데이트하는 것이다.
+> 클라이언트는 일정한 시간 간격으로 서버에 요청을 보내고, 서버는 해당 요청에 대해
+> 최신 상태의 데이터를 응답으로 보내주는 방식을 말한다. + +### 상태 관리 라이브러리의 단점? + +비동기로 상태를 변경하는 코드가 점점 추가되면 전역 상태 관리 스토어가 비대해지는데,
+단순히 상태를 변경하는 액션이 증가하는 것뿐만 아니라 전역 상태 자체도 복잡해진다. + +에러 발생, 로딩 중 등과 같은 상태는 전역으로 관리할 필요가 없다.
+이런 고민으로 인해 비동기 통신으로 react query를 사용해 처리하고 있다.
+(다만 react-query가 최적의 라이브러리는 아님, 상황에 따라 사용해야 함) + +
+
From 0d32fbbee161ad53ffcbbad8e437c77abdfcd2a0 Mon Sep 17 00:00:00 2001 From: doitchuu Date: Tue, 6 May 2025 18:50:51 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[Docs]=207.2=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../seulgi.md" | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git "a/CH07_\353\271\204\353\217\231\352\270\260_\355\230\270\354\266\234/7.2_API_\354\203\201\355\203\234_\352\264\200\353\246\254\355\225\230\352\270\260/seulgi.md" "b/CH07_\353\271\204\353\217\231\352\270\260_\355\230\270\354\266\234/7.2_API_\354\203\201\355\203\234_\352\264\200\353\246\254\355\225\230\352\270\260/seulgi.md" index dfe79c8..c9eae7a 100644 --- "a/CH07_\353\271\204\353\217\231\352\270\260_\355\230\270\354\266\234/7.2_API_\354\203\201\355\203\234_\352\264\200\353\246\254\355\225\230\352\270\260/seulgi.md" +++ "b/CH07_\353\271\204\353\217\231\352\270\260_\355\230\270\354\266\234/7.2_API_\354\203\201\355\203\234_\352\264\200\353\246\254\355\225\230\352\270\260/seulgi.md" @@ -77,10 +77,10 @@ export const userStore = new UserStore(); ## 2) 훅으로 호출하기 react-query나 useSwr과 같은 훅은 캐시를 사용해 비동기 함수를 호출하며,
-상태 관리 라이브러에서 발생했던 의도치 않은 상태 변경을 방지하는 데 도움이 된다. +상태 관리 라이브러리에서 발생했던 의도치 않은 상태 변경을 방지하는 데 도움이 된다. 예를 들어, react query에서는 onSuccess 옵션의 invalidateQueries를 사용해
-특정 키의 API를 유효하ㅣ지 않은 상태로 설정할 수 있다. +특정 키의 API를 유효하지 않은 상태로 설정할 수 있다. ```ts // hooks/usePosts.ts From 67769c74d4f10bc5fdc9f42eb11c6043e58b86c3 Mon Sep 17 00:00:00 2001 From: doitchuu Date: Tue, 6 May 2025 18:51:11 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[Docs]=207.3=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../seulgi.md" | 283 +++++++++++++++++- 1 file changed, 282 insertions(+), 1 deletion(-) diff --git "a/CH07_\353\271\204\353\217\231\352\270\260_\355\230\270\354\266\234/7.3_API_\354\227\220\353\237\254_\355\225\270\353\223\244\353\247\201/seulgi.md" "b/CH07_\353\271\204\353\217\231\352\270\260_\355\230\270\354\266\234/7.3_API_\354\227\220\353\237\254_\355\225\270\353\223\244\353\247\201/seulgi.md" index 3beced8..2b9015b 100644 --- "a/CH07_\353\271\204\353\217\231\352\270\260_\355\230\270\354\266\234/7.3_API_\354\227\220\353\237\254_\355\225\270\353\223\244\353\247\201/seulgi.md" +++ "b/CH07_\353\271\204\353\217\231\352\270\260_\355\230\270\354\266\234/7.3_API_\354\227\220\353\237\254_\355\225\270\353\223\244\353\247\201/seulgi.md" @@ -1 +1,282 @@ - +# API 에러 핸들링 + +비동기 API 에러를 구체적이고 명시적으로 핸들링하는 방법을 살펴보자! + +## 1) 타입 가드 활용하기 + +Axios 라이브러리에서는 Axios 에러에 대해 isAxiosError라는 타입 가드를 제공한다. + +타입 가드를 직접 사용하는 방법도 있지만, 서버 에러임을 명확하게 표시하고
+서버에서 내려주는 에러 응답 객체에 대해서도 구체적으로 정의함으로
+어떤 속성을 가진 에러 객체인지 파악할 수 있다. + +```ts +interface ErrorResponse { + status: string; + serverDateTime: string; + errorCode: string; + errorMessage: string; +} +``` + +- ErrorResponse 인터페이스를 사용해 처리해야 할 Axios 에러 형태는 AxiosError로 표현한다. +- 아래와 같이 타입 가드를 명시적으로 작성할 수 있다. + +```ts +function isServerError(error: unknown): error is AxiosError { + return axios.isAxiosError(error); +} +``` + +> 사용자 정의 타입 가드를 정의할 때는 타입 가드 함수의 반환 타입으로
+> parameterName is Type 형태의 타입 명제를 정의해주는 게 좋다. + +
+
+ +## 2) 에러 서브클래싱하기 + +실제 요청을 처리할 때 단순한 서버 에러도 발생하지만 다양한 에러가 발생할 수 있다. +이를 더 명시적으로 표시하기 위해 서브클래싱을 활용할 수 있다. + +> **서브클래싱**?
+> 기존 클래스를 확장해 새로운 클래스를 만드는 과정을 말한다.
+> 새로운 클래스는 상위 클래스의 모든 속성과 메서드를 상속받아 사용할 수 있고 추가적인 속성과 메서드를 정의할 수도 있다. + +예를 들어, 서버에서 전달된 메세지를 보고 개발자 입장에서는 사용자 로그인 정보가 만료되었는지,
+타임아웃이 발생한 건지 혹은 데이터를 잘못 전달한 것인지를 구분할 수 없다. + +이때, 서브클래싱을 활용하면 에러가 발생했을 때 코드상에서 어떤 에러인지를 바로 확인할 수 있다.
+또한 에러 인스턴스가 무엇인지에 따라 에러 처리 방식을 다르게 구현할 수 있다. + +
+ +### 서브클래싱으로 상세 에러 객체를 정의해보자! + +```ts +class OrderHttpError extends Error { + private readonly privateResponse: AxiosResponse | undefined; + + constructor(message?: string, response?: AxiosResponse) { + super(message); + this.name = "OrderHttpError"; + this.privateResponse = response; + } + + get response(): AxiosResponse | undefined { + return this.privateResponse; + } +} + +class NetworkError extends Error { + constructor(message = "") { + super(message); + this.name = "NetworkError"; + } +} + +class UnauthorizedError extends Error { + constructor(message?: string, response?: AxiosResponse) { + super(message, response); + this.name = "UnauthorizedError"; + } +} +``` + +### 요청 코드로 돌아와 활용해보자! + +```ts +const onActionError = ( + error: unknown, + params?: Omit +) => { + if (error instanceof UnauthorizedError) { + onUnauthorizedError( + error.message, + errorCallback?.onUnauthorizedErrorCallback + ); + } else if (error instanceof NetworkError) { + alert("네트워크 실패", { + onClose: errorCallback?.onNetworkErrorCallback, + }); + } else if (error instanceof OrderHttpError) { + alert(error.message, params); + } else if (error instanceof Error) { + alert(error.message, params); + } else { + alert(defaultHttpErrorMessage, params); + } +}; + +const getOrderHistory = async (page: number): Promise => { + try { + const { data } = await fetchOrderHistory({ page }); + const history = await JSON.parse(data); + + return history; + } catch (error) { + const customError = errorHandler(error); + onActionError(customError); + } +}; +``` + +
+
+ +## 3) 인터셉터를 활용한 에러 처리 + +Axios 같은 페칭 라이브러리는 인터셉터 기능을 제공한다. 이를 사용하면 HTTP 에러에 일관된 로직을 적용할 수 있다. + +```ts +const httpErrorHandler = ( + error: AxiosError | Error +): Promise => { + (error) => { + if (error.response && error.response.status === 401) { + window.location.href = `${backOfficeAuthHost}/login?targetUrl=${window.location.href}`; + } + + return Promise.reject(error); + }; + + orderApiRequester.interceptors.response.use( + (response: AxiosResponse) => response, + httpErrorHandler + ); +}; +``` + +
+
+ +## 4) 에러 바운더리를 활용한 에러 처리 + +- 에러 바운더리는 리액트 컴포넌트 트리에서 에러가 발생할 때 공통으로 에러를 처리하는 리액트 컴포넌트이다. +- 에러 바운더리는 에러가 발생한 컴포넌트 대신에 에러 처리를 하거나 예상치 못한 에러를 공통 처리할 때 사용할 수 있다. +- 트리 하위에 있는 컴포넌트에서 발생한 에러를 캐치하면, 해당 에러를 가장 가까운 부모 에러 바운더리에서 처리하게 할 수 있다. + +```tsx +import React, { ErrorInfo } from "react"; +import ErrorPage from "pages/ErrorPage"; + +interface ErrorBoundaryProps {} +interface ErrorBoundaryState { + hasError: boolean; +} + +class ErrorBoundary extends React.Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(): ErrorBoundaryState { + return { hasError: true }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo): void { + this.setState({ hasError: true }); + console.error(error, errorInfo); + } + + render(): React.ReactNode { + const { children } = this.props; + const { hasError } = this.state; + return hasError ? : children; + } +} + +const App = () => { + return ( + + + + ); +}; +``` + +> 위처럼 작성하면 OrderHistoryPage 컴포넌트 내에서 처리되지 않은 에러가 있을 때 에러 바운더리에서 에러 페이지를 노출한다. 이외에도 에러 바운더리에 로그를 보내는 코드를 추가하여 예상치 못한 에러의 발생 여부를 추적할 수 있게 된다. + +
+
+ +## 5) react query를 활용한 에러 처리 + +```tsx +const JobComponent = () => { + const { isError, error, isLoading, data } = useFetchJobList(); + + if (isError) { + return
{`${error.message}가 발생했습니다.`}
; + } + + if (isLoading) { + return
로딩 중
; + } + + return ( + <> + {data.map((job) => ( + + ))} + + ); +}; +``` + +
+
+ +## 6) 그 밖의 에러 처리 + +비지니스 로직에서의 유효성 검증에 의해 추가된 커스텀 에러는 200 응답과 함께 응답 바디에 별도의 상태 코드를 전달하기도 한다. + +예를 들어 장바구니에서 주문을 생성하는 API가 다음과 같은 커스텀 에러를 반환한다고 해보자. + +``` +httpStatus: 200 { + "status": "C20005", // 성공인 경우 "SUCCESS"를 응답 + "message": "장바구니에 품절된 메뉴가 있습니다" +} +``` + +> 이럴 때 만약 커스텀 에러를 사용하고 있는 요청을 일괄적으로 에러로 처리하고 싶다면 +> Axios 등의 라이브러리 기능을 활용하면 된다. + +특정 호스트에 대한 API requester를 별도로 선언하고
+상태 코드 비교 로직을 인터셉터에 추가할 수 있다. + +
+ +```ts +export const apiRequester: AxiosInstance = axios.create({ + baseURL: orderApiBaseUrl, + ...defaultConfig, +}); + +export const httpSuccessHandler = (response: AxiosResponse) => { + if (response.data.status !== "SUCCESS") { + throw new CustomError(response?.data.message, response); + } + + return response; +}; + +apiRequester.interceptors.response.use(httpSuccessHandler, httpErrorHandler); + +const createOrder = (data: CreateOrderData) => { + try { + const response = apiRequester.post("https://...", data); + } catch (error) { + // status가 SUCCESS가 아닌 경우 에러로 전달 + errorHandler(error); + } +}; +``` + +
+
From b7c3c85ea0031bde91858ccb9e321944dc0f8a12 Mon Sep 17 00:00:00 2001 From: doitchuu Date: Tue, 6 May 2025 18:59:17 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[Docs]=207.4=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../seulgi.md" | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git "a/CH07_\353\271\204\353\217\231\352\270\260_\355\230\270\354\266\234/7.4_API_\353\252\250\355\202\271/seulgi.md" "b/CH07_\353\271\204\353\217\231\352\270\260_\355\230\270\354\266\234/7.4_API_\353\252\250\355\202\271/seulgi.md" index 3beced8..ab44065 100644 --- "a/CH07_\353\271\204\353\217\231\352\270\260_\355\230\270\354\266\234/7.4_API_\353\252\250\355\202\271/seulgi.md" +++ "b/CH07_\353\271\204\353\217\231\352\270\260_\355\230\270\354\266\234/7.4_API_\353\252\250\355\202\271/seulgi.md" @@ -1 +1,35 @@ - +# API 모킹 + +프론트 개발을 하다보면 서버 API가 완성되기 전에 개발을 진행해야 할 일이 있을 때 +모킹이라는 방법을 활용할 수 있다. 모킹은 가짜 모듈을 활용하는 것을 말한다. + +모킹하는 방법은 간단히 아래와 같은 방법이 있다. + +1. JSON 파일 불러오기 +2. NextApiHandler 활용하기 +3. API 요청 핸들러에 분기 추가하기 +4. axios-mock-adapter로 모킹하기 +5. MSW와 같은 서비스워커 활용하기 + +
+
+ +## 목업 사용 여부 제어하기 + +로컬에서는 목업을 사용하고 dev나 운영 환경에서는 사용하지 않으려면 +간단한 설정을 해주면 되는데 플래그를 사용해 목업으로 개발할 때와 하지 않을 때를 구분한다. + +혹은, 스크립트 실행 시 구분 짓고자 한다면 package.json에 관련 스크립트를 추가해줄 수 있다. + +이렇게 자바스크립트 코드의 실행 여부를 제어하지 않고, +config 파일을 별도로 구성하거나 프록시를 사용할 수 있다. + +목업을 사용할 때 네트워크 요청을 확인하고 싶을 때는 네트워크에 보낸 요청을 변경해주는 +Cypress같은 도구의 웹훅을 사용하면 된다. + +> **Cypress?**
+> 프론트엔드 테스트를 위한 오픈 소스 자바스크립트 엔드 투 엔드 테스트 도구다.
+> 주로 웹 애플리케이션의 동작을 시뮬레이션하고 테스트하는 데 사용된다. + +
+