Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FE] 클라이언트 영역 에러 핸들링 개선 (토스트 UI) #419

Merged
merged 9 commits into from
Oct 24, 2024

Conversation

hwinkr
Copy link
Contributor

@hwinkr hwinkr commented Oct 24, 2024

관련 이슈

작업 내용

네트워크 요청/응답 주기에서 발생할 수 있는 예외를 전역으로 관리

기존에는 네트워크 요청/응답 주기에서 발생할 수 있는 예외에 대한 관리를 따로 하지 않았습니다. 오직 하나의 에러바운더리 컴포넌트만 있었어요. 그래서, 예외가 발생했을 경우에는 모두 폴백 UI를 통해서 피드백을 전달해 줬었습니다. 이는 사용자 경험에 좋지 못하다고 생각했어요. 그래서 토스트 UI도 생긴만큼 폴백 UI와 토스트 UI를 활용해서 예외 피드백을 더 잘 전달해 주기 위한 개선 작업을 했습니다.

export const ErrorStateContext = createContext<Error | null>(null);
export const ErrorDispatchContext = createContext<(error: Error) => void>(() => {});

export const ErrorProvider = ({ children }: PropsWithChildren) => {
  const [error, setError] = useState<Error | null>(null);

  return (
    <ErrorStateContext.Provider value={error}>
      <ErrorDispatchContext.Provider value={setError}>{children}</ErrorDispatchContext.Provider>
    </ErrorStateContext.Provider>
  );
};

전역 상태 관리 라이브러리를 사용하고 있지 않기 때문에, 전역적으로 에러 상태를 공유해줄 수 있도록 Context API를 활용했습니다. 그리고 에러 상태, 에러 상태를 변경하는 컨텍스트를 구분했어요. 그리고, useErrorState / useErrorDispatch로 추상화했습니다. 컨텍스트를 구분한 이유는 렌더링 효율 때문입니다.

export default function QueryClientManager({ children }: PropsWithChildren) {
  const setError = useErrorDispatch();

  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        throwOnError: true,                                    
      },,
      mutations: {
        onError: (error: unknown) => {
          if (error instanceof ResponseError) {
            setError(error); // -> 오직 여기서만 에러 상태를 업데이트
          }
        },
      },
    },
  });

  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}

QueryClientManager에서도 에러 상태를 구독한다면, 에러가 발생할 때마다 QueryClientManager가 다시 렌더링되기 때문에 queryClient 객체가 다시 생성되기 때문에 객체 관리에 문제가 발생합니다. 그래서 컨텍스트를 구분하기로 했어요!

// src/index.tsx

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <Global styles={globalStyles} />
    <ThemeProvider theme={theme}>
      <ErrorProvider> // -> 전역 에러 프로바이더
        <QueryClientManager> // -> 에러 상태를 업데이트, 쿼리클라이언트 객체 관리 컴포넌트
          <ReactQueryDevtools initialIsOpen={false} />
          <ToastProvider> 
            <ErrorToastNotifier /> // -> 에러 상태를 구독하고, 토스트 UI를 렌더링하는 컴포넌트
            <App />
          </ToastProvider>
        </QueryClientManager>
      </ErrorProvider>
    </ThemeProvider>
  </React.StrictMode>,
);
Before After
Screen.Recording.2024-10-24.at.16.43.49.mov
Screen.Recording.2024-10-24.at.16.45.01.mov

fetchClient 추상화 레벨 높이기 + HTTP Method 구분을 메서드명으로 할 수 있도록 하기

const createFetchClient = (baseUrl: string) => {
  return async ({ path, method, body, isAuthRequire, headers }: FetchOption) => {
    try {
      const url = `${baseUrl}${path}`;
      const response: Response = await fetch(url, {
        method,
        headers: {
          ...headers,
          'Content-Type': 'application/json',
        },
        body: body ? JSON.stringify(body) : null,
        credentials: isAuthRequire ? 'include' : 'omit',
      });

      // response 객체는 에러가 발생하면 데이터는 응답 객체가 되고, 정상적인 응답이 오면 데이터 객체가 된다.
      if (!response.ok) {
        // 응답이 에러 객체인 경우 ResponseError 객체를 생성 -> QueryClientManager 컴포넌트에서 에러 상태를 업데이트
        const errorData = await response.json();
        throw new ResponseError(errorData);
      }

      return response;
    } catch (error) {
      // catch network error
      if (error instanceof Error) {
        throw error;
      }

      throw error;
    }
  };
};
export const fetcher = {
  get: async <T>({ path, isAuthRequire }: FetcherArgs): Promise<T> => {
    const response = await fetchClient({
      path,
      method: 'GET',
      isAuthRequire,
    });

    const data = await response.json();

    return data.data as T;
  },
  post: async ({ path, body, isAuthRequire = false }: FetcherArgs) => {
    await fetchClient({ path, method: 'POST', body, isAuthRequire });
  },
  postWithResponse: async <T>({ path, body, isAuthRequire = false }: FetcherArgs): Promise<T> => {
    const response = await fetchClient({ path, method: 'POST', body, isAuthRequire });

    const data = await response.json();

    return data.data as T;
  },
  delete: async ({ path, isAuthRequire = false }: FetcherArgs) => {
    await fetchClient({ path, method: 'DELETE', isAuthRequire });
  },
};

데이터 페칭을 담당하는 fetcher 객체를 만들고, 메서드명을 HTTP 메서드명과 일치시켜서 어떤 메서드를 사용하는지 이름으로 알 수 있도록 개선했어요. 그리고, 기존에 몇몇 post 요청인 경우에 추상화된 함수 fetchClient를 사용할 수 없는 문제가 있어 자바스크립트 fetch 함수를 사용했었는데, postWithResponse 메서드를 추가로 생성해서 해결했습니다. 추가로, 위 fetchClient에서 서버에서 전달된 응답 객체(response)를 json 형태로 변경해서 반환하는 것이 아니라 서버의 응답 객체 자체를 반환하도록 수정했습니다.

그 이유는 postSchedule 함수를 호출하면 서버에서 아무런 응답이 오지 않는데, 이 때도 응답 객체를 json형태로 변경하기 위해서 response.json()을 호출하니 예외가 발생하더라구요. 그래서 응답 객체만 반환해서 fetcher 모듈에서 응답 객체가 있는 경우에만 response.json()를 호출할 수 있도록 했습니다.

약속 확정하기 페이지 이동에 확인 모달 추가하기

주최자가 약속을 확정하기 위해서, 약속 확정 페이지로 이동하기 전에 모달로 약속을 잠그고 이동할 것인지를 묻는 모달을 추가했어요. 그약속을 잠근다는 개념 자체가 우리 서비스를 이용하는 사용자들에게는 익숙하지 않은 개념이기 때문에 명시적으로 알려주기 위한 UI가 필요해서 모달을 사용했어요. 잠그는 것은 동시성 문제를 해결하기 위해서 꼭 필요하다고 판단했기 때문에, 잘 사용하게끔 유도하깅 위해서 추가했습니다.

image

특이 사항

리뷰 요구사항 (선택)

- 에러 상태와 에러 상태를 변경하는 컨텍스를 구분
- QueryClientManager 컴포넌트에서 mutation에서 에러가 발생하면 이를 감지해 에러 상태를 업데이트
- Context API에서 공유받는 데이터를 편하게 사용할 수 있도록, 커스텀 훅으로 useContext를 추상화
- fetchClient 함수에서 데이터를 반환하는 것이 아니라, 서버의 응답 객체를 반환하는 것으로 수정
- 예외가 발생했을 경우에는 ResponseError 객체를 생성해서 에러를 throw
- get, post, postWithResponse, delete 메서드 생성
- 기존 UuidContext를 사용하면 undefined로 참조되는 문제를 해결하기 위해 예외 처리
@hwinkr hwinkr added 🐈 프론트엔드 프론트엔드 관련 이슈에요 :) 🚀 기능 기능을 개발해요 :) ♻️ 리팩터링 코드를 깎아요 :) labels Oct 24, 2024
@hwinkr hwinkr added this to the 6차(최종) 데모데이 milestone Oct 24, 2024
@hwinkr hwinkr self-assigned this Oct 24, 2024
Copy link

github-actions bot commented Oct 24, 2024

Test Results

35 tests   35 ✅  25s ⏱️
 4 suites   0 💤
 1 files     0 ❌

Results for commit 2f7d3bb.

♻️ This comment has been updated with latest results.

Copy link
Contributor

@Largopie Largopie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍👍👍💯💯💯

Copy link
Contributor

@Yoonkyoungme Yoonkyoungme left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다 해리🍀 데모데이 화이팅!!

Comment on lines 27 to 28
* TODO: 응답 데이터가 없을 때도 대응 가능한 fetchClient 함수를 만들어야 함
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3]

이 주석은 이제 지워도 될 것 같네요 :)

Comment on lines +14 to +16
const data = await response.json();

return data.data as T;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3]

이 부분이 중복되는데, 아래처럼 함수로 분리해볼 수도 있을 것 같아요~!

const parseResponse = async <T>(response: Response): Promise<T> => {
  const data = await response.json();

  return data.data as T;
};

post: async ({ path, body, isAuthRequire = false }: FetcherArgs) => {
await fetchClient({ path, method: 'POST', body, isAuthRequire });
},
postWithResponse: async <T>({ path, body, isAuthRequire = false }: FetcherArgs): Promise<T> => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3]

현재 모든 함수에서 isAuthRequire = false가 기본값으로 설정되어 있네요.
fetchClient 내부에서 처리하면, fetcher에서는 isAuthRequire 관련 중복 설정을 제거할 수 있을 것 같아요!

  • fetchClient.ts
const createFetchClient = (baseUrl: string) => {
  return async ({ path, method, body, isAuthRequire = false, headers }: FetchOption) => {
    ...
  };
};

@hwinkr hwinkr merged commit 16c907a into develop Oct 24, 2024
5 checks passed
@Largopie Largopie deleted the feat/395-front-error-handling branch October 24, 2024 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
♻️ 리팩터링 코드를 깎아요 :) 🐈 프론트엔드 프론트엔드 관련 이슈에요 :) 🚀 기능 기능을 개발해요 :)
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

[FE] 모모 클라이언트 영역에서 발생할 수 있는 에러를 핸들링해요 :)
3 participants