[SSR] 서버사이드렌더링(3) - SSR 직접구현, 라우팅, styled-componenets, material-ui 사용하기 NextJS(X)

권준혁·2021년 6월 13일
4

SSR

목록 보기
3/4
post-thumbnail

지난 포스팅에 이어서 redux, styled-components, material-ui, prerendering 을 추가해보겠습니다.

소스코드: 깃허브

라우팅 구현

template/index.html

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에서 데이터를 치환하는 코드를 작성합니다.

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);

이제 브라우저에서 데이터를 사용해보겠습니다.

index.js

...tsx
const initialData = window.__INITIAL_DATA__;
ReactDom.hydrate(<App data={initialData} />, document.getElementById('root'));

App.js

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으로 접속하면 브라우저 개발자도구 콘솔에 로그가 찍힙니다.
이 데이터를 이용해서 라우팅을 구현해보겠습니다.
만약 라우트의 경로의 뎁스가 깊어지면 별도의 함수가 필요합니다.

AComponent

import React from 'react';
export default function AComponent() {
    return (
        <div> AComponent </div>
    )
}

BComponent

import React from 'react';
export default function BComponent() {
    return (
        <div> BComponent </div>
    )
}

App.js

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>
    )
}

styled-components 적용

yarn add styled-components

App.js

...
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>
    )
}

전체를 감싸고 있던 divContainer를 만들어 감싸줬습니다.
우선 서버를 다시 띄웁니다.

styled-components가 클래스에 고유한 이름을 넣고있지만, 스타일은 적용되지 않고 있습니다.
HTML문자열에 스타일도 포함되도록 서버측 코드 수정이 필요합니다.

styled-components: SSR을 참고하자

server.js

...
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컴포넌트의 스타일을 수집한 뒤 stylesgetStyleTags()함수로 수집한 스타일을 넣습니다.
getStyleTagshtml style태그를 반환합니다. 콘솔을 확인해 stylessheet를 확인해봅니다.
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파일을 수정합니다.

template/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

Material-ui도 마찬가지입니다. renderToString에 스타일포함되도록 해보겠습니다.

yarn add @material-ui/core

server.js

...
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를 사용해보겠습니다.

App.js

...
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 등 공식문서들을 참고해 작성했습니다.

소스코드: 깃허브

profile
웹 프론트엔드, RN앱 개발자입니다.

0개의 댓글