모듈 설치하기
콘솔: yarn add redux react-redux redux-thunk axios
ducks 패턴( 액션 타입, 액션 생성 함수, 리듀서 코드 한 파일에 넣어서 관리) 사용하여 리덕스 모듈 작성
src 디렉터리에 modules 디렉터리를 만들고 다음 파일을 작성
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 귀결이니까)
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);
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");
});
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()));
};
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);
*/