리액트 SSR 에서 PreloadContext 뭘까?

Felix Yi·2020년 3월 18일
0
post-thumbnail
post-custom-banner

상황과 필요

내가 작업할 소스에 express-session, cookie, next.js가 적용되어 있다. 그래서 리액트를 다루는 기술 SSR 부분을 본다. 그런데, preloadContext 부분이 한번에 이해가 안 간다.

결론

아 그냥 비동기 작업 요청의 결과인 promise 들을 한꺼번에 확인하기 위한 객체구나. 모을 때 컴포넌트 깊이에 구애받지 않기 위해 ContextAPI 의 Provider 를 사용한 거고.

Next.js 쓰면 이런 구현 고민할 필요가 없어지나?

필요 채우기 - 개관 파악

SSR 전 비동기 요청 수신 완료 여부 필요

SSR 은 미리 컴포넌트를 렌더링해서 브라우저에 내려보내주는 것. 렌더링에 필요한 자료는 미리 받아져 있어야 함. 그런데 이 자료는 비동기 요청으로 얻어오게 됨. 그럼 비동기 요청이 다 완료된 후에 컴포넌트 렌더링이 되어야 한다는 전제 필요 발생. 여튼 사전에 자료 를 적재하는 게 필요하고 영어로 하면 preload.

사전적재-SSR-CSR 흐름


출처 : https://www.reactpwa.com/docs/en/feature-ssr.html. 일부 편집 및 추가

  1. 동기 자료 요청 및 수신 & 적재완료
  2. 비동기 자료 요청 및 수신 & 적재완료
  3. 모든 사전 적재 작업들이 완료됐음을 확인
  4. 렌더링 후 클라이언트 내려줌
  5. 클라이언트에서 스토어 자료 받아서 복구
  6. 사전적재때 받아진 자료는 요청하지 않기

PRELOAD 부터 SSR 까지 탐구

아래 등장하는 코드는 리액트를 다루는 기술 깃 저장소 20장 4절 에서 발췌및 수정되었습니다.

1. 사전 적재행위는 어느 깊이의 컴포넌트에서라도 가능해야 한다. ContextAPI 사용해서 사전적재관련 작업에 사용될 컨텍스트 생성

PreloadContext.js

import { createContext, useContext } from 'react';

const PreloadContext = createContext(null);
export default PreloadContext;

해당 ContextProvider 에 기본값 넣어줌

index.server.js

  const preloadContext = {
    done: false, // 완료여부
    promises: [] // 적재된 작업들
  };

  const jsx = (
    <PreloadContext.Provider value={preloadContext}>
      <Provider store={store}>
        <StaticRouter location={req.url} context={context}>
          <App />
        </StaticRouter>
      </Provider>
    </PreloadContext.Provider>
  );

2. 사전 적재는 비동기 작업들의 결과를 적재할 수 있다.

비동기작업, 여기서는 Saga 인데, Saga는 작업의 결과가 promise 가 아니라서, 강제로 promise 를 반환하도록 설정한다.

index.server.js


 // Promise { <pending> } 을 결과로 리턴
 const sagaPromise = sagaMiddleware.run(rootSaga).toPromise(); 
// 참조1

참조1
task.toPromise() 는 fork, middleware.run, runSaga 를 사용한 실행 결과를 구체화하는 Task 인터페이스중 하나다. 실행 결과를 Promise 로 반환한다.

출처 : https://redux-saga.js.org/docs/api/

사전적재 컨텍스트에 작업을 쌓을 수 있는 함수준비

PreloadContext.js

import { createContext, useContext } from 'react';

export const Preloader = ({ resolve }) => {
  const preloadContext = useContext(PreloadContext);
  // 클라이언트에서 실행을 막도록 하는 코드
  if (!preloadContext) return null; 
  if (preloadContext.done) return null; 

  // 사전적재 컨텍스트가 미완료라면
  // 주어진 resolve() 반환값으로 then 문을 실행할 수 있는 Promise 객체를 생성해 PreloadContext.promises 에 쌓기
  preloadContext.promises.push(Promise.resolve(resolve()));
  return null;
};

위에서 만든 컴포넌트로 PreloadContext.promises 에다가 작업완료 Promise 를 적재

UsersContainer.js

const UsersContainer = ({ users, getUsers }) => {
  // CSR - 컴포넌트 마운트될 때 호출
  // SSR 에서는 useEffect 작동 안함
  useEffect(() => {
    // PreloadContext 실행으로 자료가 
    // 이미 있다면 또 요청하지 않음.
    if (users) return; 
    getUsers();
  }, [getUsers, users]);
  return (
    <>
      <Users users={users} />
      // PreloadContext 완료 아니면 getUsers 라는 비동기 작업을 실행하고 결과를 PreloadContext.promises 에 적재
      // 무조건 null 리턴하니 화면에 보이지는 않음
      <Preloader resolve={getUsers} />
    </>
  );
};

3. 사전적재 컨텍스트에 적재된 작업 결과를 기다리고 확인할 수 있다.

렌더링 시도해서 각 컴포넌트에서 비동기 요청 & 적재가 일어나도록 함

index.server.js

// renderToStaticMarkup으로 한번 렌더링합니다.
ReactDOMServer.renderToStaticMarkup(jsx);     // 렌더 과정 중 비동기작업(여기서는 saga)이 실행되며 그 결과가 PreloaderContext.promises 에 적재됨

추가 비동기 요청이 일어나지 않게 한 뒤. 적재된 작업의 결과를 확인한다.

index.server.js

store.dispatch(END); // redux-saga 의 END 액션을 발생시키면 saga 들이 모두 종료된다. saga가 종료되면 액션도 모니터링되지 않는다.

try {
  // 사가 Promise { <pending> } 이 resolved 되길 기다림
  await sagaPromise; 
  // sagaPromise 외 다른 비동기 요청 promise가 있으면 기다린다.
  await Promise.all(preloadContext.promises); 
} catch (e) {
  return res.staus(500);
}

// preloadContext 완료로 해서,
// CSR API 요청 시 있는 자료 재요청 안하도록 함
preloadContext.done = true;
};
profile
다른 누구와도 같은 시장 육체 노동자
post-custom-banner

0개의 댓글