지난 포스팅에 이어서 redux, styled-components, material-ui, prerendering 을 추가해보겠습니다.
html에
<!DOCTYPE html>
<html>
<head>
<title>test-ssr</title>
</head>
<script type="text/javascript">
window.__INITIAL_DATA__ = __DATA_FROM_SERVER__;
</script>
<body>
<div id="root"></div>
</body>
</html>
script
태그가 추가됐습니다. window전역객체의 __INITIAL_DATA__
에 데이터가 담기게됩니다.
서버에서는 문자열형태의 HTML을 전달하기 직전에 __DATA_FROM_SERVER__
를 데이터로 치환해서 보내게 됩니다.
이제 server.js
에서 데이터를 치환하는 코드를 작성합니다.
...
const app = express();
const html = fs.readFileSync(
path.resolve(__dirname, '../dist/index.html'),
'utf-8'
);
app.use('/dist', express.static('dist'));
app.get('/favicon.ico', (req,res) => res.sendStatus(204));
app.get("*", (req,res) => {
const parsedUrl = url.parse(req.url, true);
const page = parsedUrl.pathname ? parsedUrl.pathname.substr(1) : 'home';
const renderString = renderToString(<App page={page} />);
const initialData = {page};
const result = html
.replace(
'<div id="root"></div>',
`<div id="root">${renderString}</div>`
).replace('__DATA_FROM_SERVER__', JSON.stringify(initialData))
res.send(result);
});
app.listen(3000);
이제 브라우저에서 데이터를 사용해보겠습니다.
...tsx
const initialData = window.__INITIAL_DATA__;
ReactDom.hydrate(<App data={initialData} />, document.getElementById('root'));
import React from 'react';
export default function App({data}) {
console.log(page);
const onClickButton = () => {
window.alert("clicked !!")
}
return (
<div>
<button onClick={onClickButton}>클릭</button>
</div>
)
}
yarn run build && yarn run start
서버를 띄우고 localhost:3000/a
으로 접속하면 브라우저 개발자도구 콘솔에 로그가 찍힙니다.
이 데이터를 이용해서 라우팅을 구현해보겠습니다.
만약 라우트의 경로의 뎁스가 깊어지면 별도의 함수가 필요합니다.
import React from 'react';
export default function AComponent() {
return (
<div> AComponent </div>
)
}
import React from 'react';
export default function BComponent() {
return (
<div> BComponent </div>
)
}
import React from 'react';
import AComponent from './pages/AComponent';
import BComponent from './pages/BComponent';
const router = (path) => {
switch(path) {
case "a":
return AComponent;
case "b":
return BComponent;
default:
return () => <div />;
}
}
export default function App({data}) {
const onClickButton = () => {
window.alert("clicked !!")
}
const Component = router(data.page);
return (
<div>
<button onClick={onClickButton}>클릭</button>
<Component />
</div>
)
}
yarn add styled-components
...
import styled from'styled-components';
const Container = styled.div`
width: 100%;
height: 100px;
border: 1px solid rgb(200, 200, 200);
border-radius: 20px;
`;
...
export default function App({data}) {
const onClickButton = () => {
window.alert("clicked !!")
}
const Component = router(data.page);
return (
<Container>
<button onClick={onClickButton}>클릭</button>
<Component />
</Container>
)
}
전체를 감싸고 있던 div
를 Container
를 만들어 감싸줬습니다.
우선 서버를 다시 띄웁니다.
styled-components
가 클래스에 고유한 이름을 넣고있지만, 스타일은 적용되지 않고 있습니다.
HTML문자열에 스타일도 포함되도록 서버측 코드 수정이 필요합니다.
styled-components: SSR을 참고하자
...
import {ServerStyleSheet} from 'styled-components';
app.get("*", (req,res) => {
const parsedUrl = url.parse(req.url, true);
const page = parsedUrl.pathname ? parsedUrl.pathname.substr(1) : 'home';
try {
const sheet = new ServerStyleSheet();
const renderString = renderToString(sheet.collectStyles(<App data={page} />));
const styles = sheet.getStyleTags();
console.log(styles);
console.log(sheet);
const initialData = {page};
const result = html
.replace(
'<div id="root"></div>',
`<div id="root">${renderString}</div>`
).replace('__DATA_FROM_SERVER__', JSON.stringify(initialData))
.replace('__STYLE_FROM_SERVER__', styles);
res.send(result);
} catch(err) {
console.error(err)
} finally {
sheet.seal();
}
});
app.listen(3000);
server.js
의 renderString부분을 수정합니다. ServerStyleSheet
객체를 생성하고 App
컴포넌트의 스타일을 수집한 뒤 styles
에 getStyleTags()
함수로 수집한 스타일을 넣습니다.
getStyleTags
는 html style
태그를 반환합니다. 콘솔을 확인해 styles
와 sheet
를 확인해봅니다.
seal()
은 가비지컬렉션과 관련된 것인데 항상 getStyleElement()
또는 getStyleTags()
함수 이후에 실행되도록 해야합니다.그렇지 않으면 에러가 발생합니다.
이제 이 스타일을 HTML문자열에 포함시킵니다.
const result = html
.replace(
'<div id="root"></div>',
`<div id="root">${renderString}</div>`
).replace('__DATA_FROM_SERVER__', JSON.stringify(initialData))
.replace('__STYLE_FROM_SERVER__', styles);
가장 아랫줄에 __STYLE_FROM_SERVER__
를 styles데이터로 replace
하고 있습니다.
이제 index.html파일을 수정합니다.
<!DOCTYPE html>
<html>
<head>
__STYLE_FROM_SERVER__
<script type="text/javascript">
window.__INITIAL_DATA__ = __DATA_FROM_SERVER__;
</script>
<title>test-ssr</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
head
태그 안에 __STYLE_FROM_SERVER__
가 서버측에서 수집한 styled-components
의 스타일로 치환되어 전달됩니다.
스타일이 잘 동작합니다.
Material-ui도 마찬가지입니다. renderToString에 스타일포함되도록 해보겠습니다.
yarn add @material-ui/core
...
import { ServerStyleSheets as ServerStyleSheetMui } from "@material-ui/core/styles";
...
app.get("*", (req,res) => {
const parsedUrl = url.parse(req.url, true);
const page = parsedUrl.pathname ? parsedUrl.pathname.substr(1) : 'home';
const sheet = new ServerStyleSheet();
// const sheetMui = new ServerStyleSheetMui();
try {
// const renderString = renderToString(sheet.collectStyles(sheetMui.collect(<App data={page} />)));
const renderString = renderToString(sheet.collectStyles(<App data={page} />));
const styles = sheet.getStyleTags();
const initialData = {page};
const result = html
.replace(
'<div id="root"></div>',
`<div id="root">${renderString}</div>`
).replace('__DATA_FROM_SERVER__', JSON.stringify(initialData))
.replace('__STYLE_FROM_SERVER__', styles);
res.send(result);
} catch(err) {
console.error(err)
} finally {
sheet.seal();
}
});
app.listen(3000);
styled-components로 작성한 스타일과 Mui의 스타일 모두 결국 같은 스타일로서 스타일드컴포넌트 함수 sheet.collectStyles()
가 추출하고 있어 Material-ui는 따로 추출, 삽입하는 과정을 거치지 않아도 됩니다.
오히려 한번 더 추출후 삽입하게되면 HTML문자열이 중복되어 예상한 결과가 나오지 않습니다.
이제 Material-ui를 사용해보겠습니다.
...
import {Button} from '@material-ui/core';
...
export default function App({data}) {
...
return (
<Container>
<Button color="primary" variant="outlined" onClick={onClickButton}>클릭</Button>
<Component />
</Container>
)
}
html button
을 Mui의 Button
으로만 바꿨습니다.
html에도 Mui 스타일이 잘 삽입되어 있고 잘 동작합니다.
이어서 다음포스팅에서
NextJs 없이 이미지모듈 사용(서버측 웹팩사용), prerendering(성능 최적화 관련), redux, redux-saga연결에 대해 포스팅해보겠습니다.
책 실전리액트 프로그래밍과 styled-components, React, material-ui 등 공식문서들을 참고해 작성했습니다.