Skip to content

Latest commit

 

History

History
297 lines (232 loc) · 9.48 KB

README.md

File metadata and controls

297 lines (232 loc) · 9.48 KB

Ver.2) AWS Lambda / API GateWay / 무한 스크롤 / suspense

- AWS Lambda 함수 + API GateWay 로 서버리스 함수 생성

- 라이브러리 사용하지않고 suspense, infinite scroll 구현

Ver.1) 라이브러리 없이 Suspense / ErrorBoundary / Cache 구현하기

Suspense / ErrorBoundary

Suspense를 사용해 비동기로직 코드와 에러처리를 선언적으로 명시하고 중복을 줄일 수 있다.

export default function Home() {
  return (
    <ErrorBoundary>
      <Suspense fallback={<Loader />}>
        <CharacterList />
      </Suspense>
    </ErrorBoundary>
  );
}

useFetch 커스텀훅

suspense 를 사용하기 앞서 suspense에게 나의 비동기 로직 상태를 알려줘야한다.

  • pending 상태라면 페칭한 함수 반환 값인 promise 를 throw 해주고,
    (나 지금 비동기 로직 진행중~, fallback 보여주는 중)
  • promise 가 fulfilled 상태로 완료되면, 어떤 값을 resolve 해줄지,
    (비동기 끝났다~ , 해당 상태시 컴포넌트가 정상적으로 리렌더링 됨)
  • promise가 rejected 상태일때는 어떤 에러를 throw 해줄지
    (에러다! 퉤퉤, 에러바운더리에서 캐치하고 설정한 컴포넌트 렌더링 함)

1. 기본 설정

먼저, 상태와 타입을 설정.

type Status = "initial" | "pending" | "fulfilled" | "rejected";

interface UseFetch<T> {
  data?: T;
  status: Status;
  error?: Error;
  cacheKey: string;
}

Status는 요청의 현재 상태를 나타냄.

상태
initial 초기 상태
pending 데이터를 요청 중인 상태
fulfilled 데이터를 성공적으로 가져온 상태
rejected 에러가 발생한 상태

2. 컴포넌트 상태 설정

const [state, setState] = useState<UseFetch<T>>({
  data: undefined,
  status: "initial",
  error: undefined,
  cacheKey,
});

초기 상태를 설정한다.

3. useEffect 훅

데이터를 가져오는 로직을 useEffect 안에 넣는다.

useEffect(() => {
  // ...
}, [fetchingCallBackFunction, state.status, cacheKey, isCachedDataValid, setOrGetCacheData]);

4. 데이터 가져오기

데이터를 가져오는 함수.

성공하면 fulfilled 상태로 설정하고, 실패하면 rejected 상태로 설정한다.

const loadDataFromEndpoint = async () => {
  try {
    const response = await fetchingCallBackFunction();
    setOrGetCacheData(cacheKey, response);
    setState((prev) => ({ ...prev, data: response, cacheKey, status: "fulfilled" }));
  } catch (error) {
    setState((prev) => ({ ...prev, status: "rejected", error: error as Error }));
  }
};

5. 캐싱 로직

만약 데이터가 이미 캐싱되어 있다면, 다시 요청하지 않고 캐싱된 데이터를 사용.

if (state.status === "initial") {
  if (isCachedDataValid(cacheKey)) {
    setState((prev) => ({ ...prev, data: setOrGetCacheData(cacheKey), cacheKey, status: "fulfilled" }));
  } else {
    setState((prev) => ({ ...prev, status: "pending" }));
    activePromise.current = loadDataFromEndpoint(); // 데이터 페칭
  }
}

6. 서스펜스와 에러 처리

pending 상태에서는 Promise를 throw하여 Suspense가 이를 인식하여 내가 설정한 fallback을 보여준다.

rejected 상태에서는 Error를 throw하여 에러바운더리에서 에러를 캐치해서 처리할 수 있게 한다.

if (state.status === "pending" && activePromise.current) {
  throw activePromise.current; // promise를 저장한 ref.current값
}

if (state.status === "rejected" && state.error) {
  throw state.error;
}
export default function Home() {
  return (
    <ErrorBoundary>
      {" "}
      //에러 발생시 캐치캐치 나이스캐치
      <Suspense fallback={<Loader />}>
        {" "}
        // pending 상태일때는 <Loader />를 보여줌
        <CharacterList />
      </Suspense>
    </ErrorBoundary>
  );
}

ErrorBoundary

사실 공식문서에 코드가 존재한다. : 공식문서

Catching rendering errors with an error boundary

  • 공식문서 코드

    class ErrorBoundary extends React.Component {
      constructor(props) {
        super(props);
        this.state = { hasError: false };
      }
    
      static getDerivedStateFromError(error) {
        // Update state so the next render will show the fallback UI.
        return { hasError: true };
      }
    
      componentDidCatch(error, info) {
        // Example "componentStack":
        //   in ComponentThatThrows (created by App)
        //   in ErrorBoundary (created by App)
        //   in div (created by App)
        //   in App
        logErrorToMyService(error, info.componentStack);
      }
    
      render() {
        if (this.state.hasError) {
          // You can render any custom fallback UI
          return this.props.fallback;
        }
    
        return this.props.children;
      }
    }

자바스크립트 기준으로 코드가 존재하기때문에 추천하는 react-error-boundary 라이브러리 방문해서 타입들을 참고해서 만들었다.
(사실 그렇게 하지않아도 될정도의 단순한 타입이긴함!)

import { Component, ErrorInfo, ReactNode } from "react";

import DefaultErrorBoundary from "./DefaultErrorBoundary";

type ErrorBoundaryProps = {
  children: ReactNode;
  fallback?: ReactNode;
};

type ErrorBoundaryState = {
  hasError: boolean;
  error?: Error;
};

class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
  constructor(props: ErrorBoundaryProps) {
    super(props);
    this.state = { hasError: false, error: undefined };
  }
  static getDerivedStateFromError(error: Error) {
    // 다음 렌더링에서 폴백 UI가 보이도록 상태를 업데이트 합니다.
    return { hasError: true, error };
  }

  public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    // 에러 리포팅 서비스에 에러를 기록할 수도 있습니다.
    console.error(error, errorInfo);
  }

  public render() {
    if (this.state.hasError && this.state.error) {
      // 폴백 UI를 커스텀하여 렌더링할 수 있습니다.
      return this.props.fallback || <DefaultErrorBoundary error={this.state.error} />;
      // 에러바운더리를 사용할때 프롭으로 넘겨주는 폴백이 없다면 기본 에러바운더리를 사용
      // 어떤 에러인지 에러메시지 자체를 프롭으로 넘겨서 렌더하게함
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

실제로 잘못된 URI로 인해서 에러바운더리가 동작하게 될 때의 화면

caching

  1. 인메모리로 캐싱
  2. 객체리터럴이 아닌 Map을 사용하고 메서드를 이용
  3. 모든 페이지에서 페칭한 데이터를 캐싱해두기 위해서 contextAPI로 최상단에서 context를 묶기
  4. 엔드포인트로 캐시키를 설정
function App() {
  return (
    <CacheContextProvider>
      <Layout>
        <Outlet />
      </Layout>
    </CacheContextProvider>
  );
}
/* eslint-disable @typescript-eslint/no-explicit-any */
const ONE_MINUTE_MS = 60 * 1000;

const getCachedDataOrSet = (cacheExpirationDuration: number = ONE_MINUTE_MS * 10) => {
  const cache = new Map<string, any>();
  return {
    setOrGetCacheData: (key: string, data?: any) => {
      if (cache.has(key)) {
        const { data, expireTime } = cache.get(key);
        if (expireTime > Date.now()) {
          return data;
        }
      }
      cache.set(key, { data, expireTime: Date.now() + cacheExpirationDuration });
      return data;
    },
    isCachedDataValid: (key: string) => {
      if (!cache.has(key)) return false;
      const { expireTime } = cache.get(key);
      return expireTime > Date.now();
    },
  };
};

export default getCachedDataOrSet;
식별자 명 내용
setOrGetCacheData - 캐싱되어있거나 캐싱한 데이터를 반환
1. 만약 캐싱되어 있다면 만료시간을 체크하고 유효하다면 캐싱된 값을 반환
2. 캐싱된 데이터의 만료시간이 지나갔으면 다시 캐싱하건, 캐싱되어 있지 않는 경우에는 새로 캐싱
isCachedDataValid - boolean 반환
3. 이 캐시키에 데이터가 캐싱되어있을까?
4. 만약 캐싱이 되어있다면, 만료시간이 지나지 않았는지?