서버사이드 렌더링(2)

리린·2021년 8월 2일
0

문제점: 데이터 로딩

  • 서버에서는 자동 리렌더링이 불가능함( 컴포넌트 x. 문자열 형태 렌더링이기 때문에)
  • useEffect나 componentDidMount에서 설정한 작업 등이 호출되지 않음

준비작업: redux-thunk

  1. 모듈 설치하기
    콘솔: yarn add redux react-redux redux-thunk axios

  2. ducks 패턴( 액션 타입, 액션 생성 함수, 리듀서 코드 한 파일에 넣어서 관리) 사용하여 리덕스 모듈 작성

  3. src 디렉터리에 modules 디렉터리를 만들고 다음 파일을 작성

  • modules/users.js
  1. 평범하게 thunk와 redux를 사용하여 리액트 앱 만들기

해결방법:PreloadContext

  1. 만들기
  • src/lib/PreloadContext.js
import { createContext, useContext } from "react";

//클라이언트 환경: null
//서버환경: {done: false, promises:[]}

const PreloadContext = createContext(null);

export default PreloadContext;

//resolve는 함수 타입

export const Preloader = ({ resolve }) => {
  const preloadContext = useContext(PreloadContext);
  if (!preloadContext) return null; // context값이 유효하지 않다면 아무것도 하지 않음
  if (preloadContext.done) return null; //이미 작업이 끝났다면 아무것도 하지 않음
  preloadContext.promises.push(Promise.resolve(resolve()));
  return null;
};
  • preloadContext :
    서버사이드 렌더링을 하는 과정에서 처리해야 할 작업들을 실행.
    프로미스가 존재하면 프로미스가 끝날 때까지 대기
    렌더링 뒤 데이터가 채워진 상태로 컴포넌트가 나타남.

  • preloader 컴포넌트
    resolve 함수를 props로 받아옴
    컴포넌트가 렌더링될 때 서버 환경에서만 resolve함수를 호출한다.
    (promise 귀결이니까)

  1. container에 적용하기
  • containers/UsersContainer.js
import React, { useEffect } from "react";
import Users from "../components/Users";
import { connect } from "react-redux";
import { getUsers } from "../modules/users";
import { Preloader } from "../lib/PreloadContext";

const UsersContainer = ({ users, getUsers }) => {
  useEffect(() => {
    if (users) return;
    getUsers();
  }, [getUsers, users]);
  return (
    <>
      <Users users={users} />
      <Preloader resolve={getUsers} />
    </>
  );
};

export default connect(
  (state) => ({
    users: state.users.users,
  }),
  {
    getUsers,
  }
)(UsersContainer);
  1. 서버에서 리덕스 설정/ PreloadContext 사용하기
  • src/index.server.js
import React from "react";
import ReactDOMServer from "react-dom/server";
import express from "express";
import { StaticRouter } from "react-router-dom";
import App from "./App";
import path from "path";
import fs from "fs";
import { createStore, applyMiddleware } from "react-redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
import rootReducer from "./modules";
import PreloadContext from "./lib/PreloadContext";
import Red from "./components/Red";

const manifest = JSON.parse(
  fs.readFileSync(path.resolve("./build/asset-manifest.json"), "utf8")
);

const chunks = Object.keys(manifest.files)
  .filter((key) => /chunk\.js$/.exec(key)) //chunk.js 로 끝나는 키를 찾아서
  .map((key) => `<script src="$manifest.files[key]"></script>`) //스크립트 태그로 변환하고
  .join(""); //합쳐

function createPage(root, stateScript) {
  return `<!DOCTYPE html> 
  <html lang='en'>
  <head>
  <meta charset="utf-8"/>
  <link rel="shortcut icon" href="/favicon.ico"/>
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
  <meta name="theme-color
 content="#000000"/>
 <title> react app</title>
 <link href="${manifest.files["main.css"]}" rel="stylesheet"/>
 </head>
 <body>
 <noscript>You need to enable js to run this app</noscript>
 <div id="root">
 ${root}
 </div>
 ${stateScript}
 <script src="${manifest.files["runtime-main.js"]}"></script>
 ${chunks}
 <script src="${manifest.files["main.js"]}"></script>
 </body>
 </html> `;
}
const app = express();

//서버사이드 렌더링을 처리할 핸들러 함수

const serverRender = async (req, res, next) => {
  // 404를 띄우는 상황이 와도 서버사이드렌더링을 해줌
  const context = {};
  const store = createStore(rootReducer, applyMiddleware(thunk));
  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>
  );
  ReactDOMServer.renderToStaticMarkup(jsx);
  try {
    await Promise.all(preloadContext.promises);
  } catch (e) {
    return res.status(500);
  }
  preloadContext.done = true;
  const root = ReactDOMServer.renderToString(jsx);
  //json이 문자열로 변환하고 악성 스크립트가 실앻ㅇ되는 것을 방지하기 위해 <를 치환처리
  const stateString = JSON.stringify(store.getState()).replace(/</g, "\\u003c");
 // 리덕스 초기 상태를 스크립트로 주입함 
  const stateScript = `<script>__PRELOADED_STATE__=${stateString}</script>`;
  res.send(createPage(root, stateScript));
};

const serve = express.static(path.resolve("./build"), {
  index: false,
});

//순서가 중요: serve 다음에 serverRender
app.use(serve);
app.use(serverRender);

app.listen(5000, () => {
  console.log("Running on http://localhost:5000");
});
  • 주의사항: 요청이 들어올 때마다 새로운 스토어를 만든다.
  • renderToStaticMarkup 함수: 리액트를 사용하여 정적인 페이지를 만들 때 사용함.
    빠르지만 클라이언트 사이드에서 html dom 인터랙션을 지원하기 어려움
    실은, 단지 Preloader 로 넣어줬던 함수를 호출하기 위해 사용함

usePreloader 커스텀 함수 만들기

  • lib/PreloadContext.js 파일에 usePreloader 추가
import { createContext, useContext } from "react";

//클라이언트 환경: null
//서버환경: {done: false, promises:[]}

const PreloadContext = createContext(null);

export default PreloadContext;

//resolve는 함수 타입

export const Preloader = ({ resolve }) => {
  const preloadContext = useContext(PreloadContext);
  if (!preloadContext) return null; // context값이 유효하지 않다면 아무것도 하지 않음
  if (preloadContext.done) return null; //이미 작업이 끝났다면 아무것도 하지 않음
  preloadContext.promises.push(Promise.resolve(resolve()));
  return null;
};

//추가됨
export const usePreloader = (resolve) => {
  const preloadContext = useContext(PreloadContext);
  if (!preloadContext) return null;
  if (preloadContext.done) return null;
  preloadContext.primises.push(Promise.resolve(resolve()));
};
  • container/UserContainer.js 사용
import React, { useEffect } from "react";
import Users from "../components/Users";

import { getUser } from "../modules/users";
import { usePreloader } from "../lib/PreloadContext";
import { useSelector, useDispatch } from "react-redux";

const UserContainer = ({ id }) => {
  const user = useSelector((state) => state.users.user);
  const dispatch = useDispatch();
  usePreloader(() => dispatch(getUser(id)));
  useEffect(() => {
    if (user && user.id === parseInt(id, 10)) return;
    dispatch(getUser(id));
  }, [dispatch, id, user]);
  if (!user) return null;
  return <user user={user} />;
};

export default UserContainer;

/** /
기존코드 
import React, { useEffect } from "react";
import Users from "../components/Users";
import { connect } from "react-redux";
import { getUsers } from "../modules/users";
import { Preloader } from "../lib/PreloadContext";



const UsersContainer = ({ users, getUsers }) => {
  useEffect(() => {
    if (users) return;
    getUsers();
  }, [getUsers, users]);
  return (
    <>
      <Users users={users} />
      <Preloader resolve={getUsers} />
    </>
  );
};

export default connect(
  (state) => ({
    users: state.users.users,
  }),
  {
    getUsers,
  }
)(UsersContainer);

*/
profile
개발자지망생

0개의 댓글