공부내용 공유하기

[NextJS] getInitialProps에서 redux-saga 사용하기

CSR에서는, 페이지가 로드되고 난 다음 ajax요청을 통해 값을 가져오고 그 데이터를 뿌려준다.

 

[클라이언트] 페이지 접근

[서버] html, js 전송

[클라이언트] 빈 html, 필요한 js파일 받음

[클라이언트] js 로드

[클라이언트] 필요한 데이터 요청(ajax)

[클라이언트] 결과물 반환

[클라이언트] 렌더링

 

그러나 SSR에서는 SEO등의 목적으로 인해 클라이언트에 페이지를 전송하기 전에 데이터 요청을 해야 할 상황이 생긴다.

 

[클라이언트] 페이지 접근

[서버] 페이지 렌더링에 필요한 데이터 ajax 요청*

[서버] 결과물 반환

[서버] 렌더링

[서버] html로 변환

[서버] html 전송

[클라이언트] 받음

 

 

그렇다면 다음과 같은 상황은 어떨까?

 

예를 들어 제품 상세 페이지가 있다고 해보자.

그리고 상세 페이지에서 보여줘야 하는 데이터는 다음과 같이 3가지가 있다.

 

1. 제품의 디테일한 정보

2. 비슷한 제품의 정보

3. 제품의 재고 현황

 

각 데이터의 요청에 걸리는 시간은 0.5초~1초 정도로 결코 짧지 않다고 가정한다.

 

개발자는 제품 상세 페이지 내에서의 유저 경험을 손상시키고 싶지 않기 때문에, 다음과 같은 전략을 취한다.

 

= 목록에서 제품 상세 페이지로 들어갈 때에는 CSR로 동작하게 한다.

  - 목록에서 접근 시에는 페이지가 마운트 된 이후에 데이터를 요청한다.

  - 데이터를 불러오는 동안, 데이터 위치에는 스켈레톤 UI를 노출해서 유저 경험을 향상한다.

  - 데이터가 준비되면 스켈레톤 UI를 제거하고 데이터를 보여준다.

 

= 제품 상세 페이지 링크로 접근할 경우에는 SSR로 동작하게 한다.

  - URL로 접근 시에는 SEO 최적화를 위해 3가지의 모든 데이터가 로드된 페이지를 제공한다.

  - 스켈레톤 UI 등이 없으며, 서버에서 모든 데이터를 확보하여 렌더링 한다.

 

 

그리고 이 로직들을 redux-saga를 활용하여 CSR이던, SSR이던 동일한 로직을 태우고 싶다면, 아래와 같은 방법이 필요하다.

 

시작하기에 앞서, 기본적으로 next-redux-wrapper를 통해 next와 redux store의 연결이 끝난 상태라고 가정한다.

 

 

 

kirill-konshin/next-redux-wrapper

Redux wrapper for Next.js. Contribute to kirill-konshin/next-redux-wrapper development by creating an account on GitHub.

github.com

(이와 같은 상태)

// _app.tsx
//...
//...
export default wrapper.withRedux(NextApp);

ProductDetail.tsx 하단부

// ? getInitialProps => 클라이언트로 접근하던, 서버로 접근하던 공통적으로 거치는 부분

ProductDetail.getInitialProps = async (ctx: NextPageContext<any, AnyAction>) => {
  const { store, req } = ctx;
  // 컨텍스트에 들어있는 store 추출
  
  const storeState: RootState = store.getState();
  const { dispatch } = store;
  
  // isFetchSuccess 가 false라면(CSR이라면), mount이후 데이터를 다시 로드한다.
  let isFetchSuccess = false;
  
  // req가 있으면 서버
  if (req) {
    // ? 상품 데이터 가져오기
    await store.execSagaTasks(true, (dispatcher) => {
      dispatcher(getProductData());
    });
    isFetchSuccess = true;
  }
  return { isFetchSuccess };
};

 

store.execSagaTasks는 여길 참고하면 된다.

 

 

Using Redux Saga with Next.js

Redux is a popular state management library to use with React app. It provides a single store to store all of the application data. All…

medium.com

내가 이해한 부분을 바탕으로 store생성시에 추가한 내용은 아래와 같다.

// redux/store.ts

  //....
  
  // * Store type override -> runSaga, stopSaga, execSagaTasks * //
  store.runSaga = () => {
    if (store.saga) return;
    // ! 재 실행 방지
    store.saga = saga.run(rootSaga);
  };

  store.stopSaga = async () => {
    if (!store.saga) return;
    // ! 재 실행 방지
    store.dispatch(END); // ? END 액션을 디스패치
    // ? --> TAKE 를 기다리는 사가를 다 종료시켜서 사가의 iterator 상태를 done 으로 변경
    await store.saga.toPromise(); // ? done 상태 반환을 기다림
    store.saga = null; // ? 사가 초기화
  };
  
  store.execSagaTasks = async (isServerEnv, tasks) => {
    store.runSaga(); // ? 사가 실행
    tasks(store.dispatch); // ? 인자로 전달받은 task 는 dispatch 를 받아서 액션 dispatch
    await store.stopSaga(); // ? 사가 실행을 멈추고 작업중이던 비동기 처리가 완료되길 기다림.

    if (!isServerEnv) {
      // ? 클라이언트에서 사가 재실행
      store.runSaga();
    }
  };
  
  
  //....

 

store type 내에 runSaga, stopSaga, execSagaTasks는 없기 때문에, type에러를 보기 싫다면 타입을 별도로 선언해서 만들어줘야 한다.

 

이렇게 적용하면 CSR이던 SSR이던 상관없이, 페이지 내에서 useSelector를 사용해서 store 내부의 값만 바라보면 된다.

 

그리고 페이지 컴포넌트 마운트 이후, 필요한 데이터를 요청하는 로직도 SSR과 동일하게 가져갈 수 있다.