먼저 redux공식문서
를 참고해서 작성해보겠습니다..
서버사이드 렌더링에서 리덕스를 구현할 때, 서버의 역할은 유저의 첫 요청이나 검색엔진 크롤러의 요청에 대해 초기 상태값을 전달해주는 것 입니다.
이전 글에서 사용했던 프로젝트에 이어서 하겠습니다.
먼저 필요한 모듈들을 설치합니다.
yarn add redux react-redux
해야할 일들을 정리해보겠습니다. 서버는 각 요청에대해 매번 새로운 스토어 인스턴스를 생성해야합니다.
클라이언트 리덕스 적용
서버의 HTML string
서버에선 요청을 핸들링하는 미들웨어 또는 엔드포인트의 내부에 createStore
메서드를 실행시키고, renderToString
함수에 App
컴포넌트를 Provider
로 감싸 전달합니다 Provider
의 props로 store
가 전달됩니다.
HTML파일 수정
HTML파일의 스크립트에서 window객체 내에 저장할 store데이터를 작성합니다. (예를들어 HTML의 스크립트태그 내에window.__PRELOADED_STATE__ = __REDUX_STATE_FROM_SERVER__;
)
클라이언트
클라이언트에서 hydrate
함수가 실행되기 전 전달받은 초기 상태값을 이용해서 store
를 생성하고 hydrate
함수를 실행합니다.
리덕스 환경을 구축하는 것이 주 목적이 아니기 때문에 간단하게 구축해보겠습니다.
Redux 상태값을 이용해서 UI테마를 변경하는 시나리오입니다.
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가 변경됩니다.
import { combineReducers } from "redux";
import common from './common/state';
const rootReducer = combineReducers({
common,
});
export default rootReducer;
모듈화를 위해 사용합니다.
createStore
는 서버로부터 전달받은 데이터를 이용해서 할 것이기 때문에 나중에 작성합니다.
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-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>
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'));
redux의 dispatch함수를 실행해보고 잘 동작하는지 확인해보겠습니다.
...
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와 이미지 모듈사용 환경을 구축해보겠습니다.
도움이 됬어요 감사합니다