[SSR] 서버사이드렌더링(4) - SSR 직접구현 (redux 연결)

권준혁·2021년 7월 4일
3

SSR

목록 보기
4/4
post-thumbnail

redux공식문서: Server Rendering

1. 리덕스 연결

먼저 redux공식문서를 참고해서 작성해보겠습니다..
서버사이드 렌더링에서 리덕스를 구현할 때, 서버의 역할은 유저의 첫 요청이나 검색엔진 크롤러의 요청에 대해 초기 상태값을 전달해주는 것 입니다.

이전 글에서 사용했던 프로젝트에 이어서 하겠습니다.

먼저 필요한 모듈들을 설치합니다.

yarn add redux react-redux

해야할 일들을 정리해보겠습니다. 서버는 각 요청에대해 매번 새로운 스토어 인스턴스를 생성해야합니다.

  1. 클라이언트 리덕스 적용

  2. 서버의 HTML string
    서버에선 요청을 핸들링하는 미들웨어 또는 엔드포인트의 내부에 createStore메서드를 실행시키고, renderToString함수에 App컴포넌트를 Provider로 감싸 전달합니다 Provider의 props로 store가 전달됩니다.

  3. HTML파일 수정
    HTML파일의 스크립트에서 window객체 내에 저장할 store데이터를 작성합니다. (예를들어 HTML의 스크립트태그 내에window.__PRELOADED_STATE__ = __REDUX_STATE_FROM_SERVER__;)

  4. 클라이언트
    클라이언트에서 hydrate함수가 실행되기 전 전달받은 초기 상태값을 이용해서 store를 생성하고 hydrate함수를 실행합니다.


1-1 클라이언트 리덕스 적용

리덕스 환경을 구축하는 것이 주 목적이 아니기 때문에 간단하게 구축해보겠습니다.
Redux 상태값을 이용해서 UI테마를 변경하는 시나리오입니다.

  • state.js
export const types = {
    SET_THEME_COLOR: "common/SET_THEME_COLOR",
};
export const actions = {
    setThemeColor: (payload) => ({
        type: types.SET_THEME_COLOR,
        payload,
    })
}
export const INITIAL_STATE = {
    themeColor: 'light',
};

const reducer = (state = INITIAL_STATE, action) => {
    switch (action.type) {
        case types.SET_THEME_COLOR: {
            state.themeColor = action.payload;
        }
    }
    return state;
}

export default reducer;

dispatch 되는 action payload에 따라 themeColor가 변경됩니다.

  • reducer.js
import { combineReducers } from "redux";
import common from './common/state';

const rootReducer = combineReducers({
    common,
});
 
export default rootReducer;

모듈화를 위해 사용합니다.
createStore는 서버로부터 전달받은 데이터를 이용해서 할 것이기 때문에 나중에 작성합니다.


1-2. 서버의 HTML string

  • server.js
app.get("*", (req,res) => {

    const store = createStore(rootReducer);	// 추가
  
	...
    
    try {
        const renderString = renderToString(sheet.collectStyles(
            <Provider store={store}>
                <App data={page} />
            </Provider>
        ));

        const preloadedState = JSON.stringify(store.getState());
  
		...
        
        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)
        .replace('__REDUX_STATE_FROM_SERVER__', preloadedState);

        res.send(result);

    } catch(err) {
        console.error(err)
    } finally {
        sheet.seal();
    }
});

각 요청에 대해 매번 새로운 store객체가 생성되도록 엔드포인트 최상단에 createStore를 실행시켜 새로운 객체를 사용하고 있습니다.
매번 새로운 객체를 생성하는 이유는 포스트 상단에 SSR에서 서버의 역할과 관련이 있습니다.
renderToString 즉 초기 전달되는 HTML string에서도 리덕스를 포함시켜 초기에 전달받는 화면이 hydrate된 화면과 일치하도록 합니다.
preloadedState를 html의 __REDUX_STATE_FROM_SERVER__로 대치시킵니다.
클라이언트의 윈도우 객체에 저장시킨 __REDUX_STATE_FROM_SERVER__는 hydrate되는 시점에서 앱의 초기 상태값으로 사용됩니다.

이어서 HTML파일을 수정합니다.

1-3. HTML파일 수정

1-2 에서 .replcae()함수를 이용해 __REDUX_STATE_FROM_SERVER__를 수정했었습니다. 올바르게 동작되도록 html파일도 수정합니다.

<!DOCTYPE html>
<html>
    <head>
        __STYLE_FROM_SERVER__
        <script type="text/javascript">
            window.__INITIAL_DATA__ = __DATA_FROM_SERVER__;
        </script>
        <script type="text/javascript">
            window.__PRELOADED_STATE__ = __REDUX_STATE_FROM_SERVER__;
        </script>
        <title>test-ssr</title>
    </head>
    <body>
        <div id="root"></div>
    </body>
</html>

1-4 클라이언트 hydrate

hydrate함수가 실행되는 시점은 유저가 html스트링을 통해 인터렉션이 없지만 화면은 보이는 상태일 때입니다.
hydrate를 이용해 이벤트 주입을 할 때, 서버측에서 보낸 리덕스 초기 상태값으로 store가 생성되도록 합니다.

import React from 'react';
import ReactDom from 'react-dom';
import App from './App';
import { Provider } from 'react-redux';
import {createStore} from 'redux';
import rootReducer from './redux/reducer';

const initialData = window.__INITIAL_DATA__;
const preloadedState = window.__PRELOADED_STATE__;
delete window.__PRELOADED_STATE__;
const store = createStore(rootReducer, preloadedState);

ReactDom.hydrate(
    <Provider store={store}>
        <App data={initialData} />
    </Provider>
    , document.getElementById('root'));

2. 확인

redux의 dispatch함수를 실행해보고 잘 동작하는지 확인해보겠습니다.

  • App.js
...
import {useDispatch, useSelector} from 'react-redux';
import {actions} from './redux/common/state';

const Container = styled.div`
    width: 100%;
    height: 100px;
    border: 1px solid rgb(200, 200, 200);
    border-radius: 20px;
    background-color: ${props => props.theme === "dark" ? "rgb(40,40,40)" : "rgb(240,240,240)"};
    color: ${props => props.theme === "dark" ? "rgb(240,240,240)" : "rgb(40,40,40)"};
`;

const router = 
...

export default function App({data}) {
    const dispatch = useDispatch();
    const theme = useSelector(state => state.common.themeColor);
    
    const changeThemeColor = () => {
        dispatch(actions.setThemeColor(theme === "dark" ? "light" : "dark"));
    }
    const Component = router(data.page);
    return (
        <Container theme={theme}>
            <Button color="primary" variant="outlined" onClick={changeThemeColor}>{theme}</Button>
            <Component />
        </Container>
    )
}


버튼 이벤트를 통해 dispatch함수가 실행되고, 변경된 스토어의 themeColor에 따라 UI도 변경됩니다.
SSR에서 리액트 컴포넌트 이벤트와 redux dispatch, store상태값에 따른 styled-components의 속성값 변경도 잘 이루어집니다.

이어서 redux-saga와 이미지 모듈사용 환경을 구축해보겠습니다.

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

2개의 댓글

comment-user-thumbnail
2022년 3월 24일

도움이 됬어요 감사합니다

1개의 답글