이전에 동일한 JSX를 갖고 html과 React App을 렌더링하고, 이 둘을 연결시켜보았다.
이번에는 Next(12버전)의 핵심 기능중 하나인 getServerSideProps를 구현하고, 이걸 클라이언트와 연동시켜보고자 한다.
getServerSideProps
아래는 Next docs에서 가져온 사진이다.
이걸 어떻게 사용하는지는 이미 알고있다고 가정하고, 이 코드들의 특성을 분석해보자.
이 코드를 보면 네가지를 알 수 있다.
- getServerSideProps는 page컴포넌트에 작성되어야한다.
- 함수 이름은 getServerSideProps이며, export되어야한다.
- page컴포넌트에 getServerSideProps에서 반환된 값을 props로 넘겨준다.
- page컴포넌트 자체는 default export되어야한다.
무작정 적용하기
일단 위의 예시처럼 App과 Content에 Server Side Props를 넘겨주고, 이걸 useState의 초기값으로 넘겨보자.
App.tsx
import Content from '../components/Content';
import Header from '../components/Header';
interface AppProps {
number?: number;
}
export const getServerSideProps = () => {
return {
props: {
number: 100,
},
};
};
export default function App({ number }: AppProps) {
return (
<div style={{ width: '100%' }}>
<Header />
<Content serverNumber={number} />
</div>
);
}
Content
import { useState } from '@core/useState';
export default function Content({ serverNumber }: { serverNumber?: number }) {
const [number, setNumber] = useState<number>(serverNumber || 0);
return (...)
}
구현하기
이제 이걸 어떻게 적용시킬지 고민해보자.
getServerSideProps를 가져오기
이제 앞에서 생각했던 네가지를 고려할때다.
- getServerSideProps는 page컴포넌트에 작성되어야한다.
- 함수 이름은 getServerSideProps이며, export되어야한다.
- page컴포넌트에 getServerSideProps에서 반환된 값을 props로 넘겨준다.
- page컴포넌트 자체는 default export되어야한다.
이걸 활용해보자.
getServerSideProps와 page default 함수는 모두 pages 안에 작성되어야한다.
따라서 pages의 파일을 읽어와서, require를 통해 동적으로 모듈을 가져올 수 있다.
동적으로 getServerSideProps를 가져와보자.
import { readdir } from 'fs';
import path from 'path';
const PAGES_PATH = 'dist/pages';
type ServerSideFunction = Record<string, Function>;
export const getServerSidePropsFunction = async () => {
const result: ServerSideFunction = {};
const files: string[] = await new Promise((resolve, reject) => {
//runtime에 pages 디렉토리를 훑게되므로 블로킹을 고려한다.
readdir(PAGES_PATH, (err, files) => {
if (err) reject();
resolve(files); //파일목록을 읽어온다. ex) files=["index.js"]
});
});
files.map((file) => {
const { getServerSideProps } = require(
path.resolve(`${PAGES_PATH}/${file}`) //ex) dist/pages/index.js
);
result[file] = getServerSideProps; //객체에 함수를 등록한다.
});
/**
result= {
"App.js":getServerSideProps;
}
**/
return result;
};
pages 내부의 모든 파일을 가져와서 그중에 getServerSideProps라는 모듈을 찾아온다.
이후에 이걸 fileName프로퍼티의 값으로 붙여서 내보내주게 되면, 해당 객체내부의 함수만 호출하면 serverProps를 가져올 수 있게 된다!
이러면 이제 serverProps를 내려줄 수는 있는데 serverProps가 적용은 되지 않은 것을 확인할 수 있다.
왜냐하면 클라이언트에서 hydrate할때 serverProps가 적용되지 않기때문이다.
public/index.tsx
이 함수가 클라이언트가 받게되는 js파일이된다.
import App from '../src/components/App';
import { hydrate } from '@core/render';
hydrate(<App />, document.getElementById('_miniNext'));
이전 hydrate함수
export const hydrate = (content: MiniReactNode, container: HTMLElement) => {
console.log('hydrate 완료');
const element = makeDOM(content);
root = container;
prev = content;
container.innerHTML = '';
container.appendChild(element);
callEffects(); // DOM이 생성된 이후에 useEffect를 실행시킨다.
};
여기서 보면 content가 App함수를 호출한 결과인데, public/index.tsx에서 호출할때 따로 ServerProps를 전달해주지 않고 있기 때문이다.
hydrate (ServerSideProps)
그렇다면 ServerProps를 클라이언트 파일도 접근할 수 있게 만들면된다.
how?
나는 서버사이드에서 html을 생성할때, script에서 window객체에 serverSideProps를 담아서 보내주도록 했다
그렇다면 createHTML의 인자로 serverSideProps를 받을 수 있어야한다.
createHTML
export const createHTML = (
element: MiniReactNode,
initialServerProps?: Record<string, string> //serverProps를 인자로 받았다.
) => {
const root = `
<html>
<head>
<title>MiniNext</title>
</head>
<body>
<div id="_miniNext">${_createHTML(element)}</div>
</body>
<script>
window._miniNextData=${JSON.stringify(initialServerProps)} //window._miniNextData에 저장한다.
</script>
<script src='index.js' type="module"></script>
</html>`;
return root;
};
app.tsx
따라서 app.tsx도 아래처럼 바뀌게 된다.
app.get('/', async (req, res) => {
const serverSideObject = await getServerSidePropsFunction();
const serverSideProps = serverSideObject["App.js"]().props
const html = createHTML(App(serverSideProps), serverSideProps);
res.send(html);
});
그럼 이제 window._miniNextData에 넣어준 데이터를 miniReact의 hydrate과정에서 props로 넣어주면된다.
hydrate
export const hydrate = (Component: Function, container: HTMLElement) => {
const content = Component(window._miniNextData); //ServerSideProps연동
console.log('hydrate 완료');
const element = makeDOM(content);
root = container;
prev = content;
container.innerHTML = '';
container.appendChild(element);
callEffects();
};
원래는 <App/>처럼 호출이 완료된 함수 객체, miniReactNode를 인자로 받았지만, props를 hydrate함수에서 넣어주기위해서 인자 타입을 변경해주었다.
여기서 window._miniNextData의 값을 App의 props로 넣어주게된다.
public/index.tsx
import App from '../src/pages/App';
import { hydrate } from '@core/render';
hydrate(App, document.getElementById('_miniNext')); //<App/>에서 App으로 함수 객체자체를 넘긴다.
결과
내려준 ServerSideProps가 잘 적용된 모습이다!
export const getServerSideProps = async () => {
const number = await new Promise((resolve) => {
resolve(100);
});
return {
props: {
number,
},
};
};
요런식으로 getServerSideProps함수를 변경해도 잘 동작하는 모습이다.
+) 실제 Next
__NEXT_DATA__에 데이터를 담아서 보내준다. 비슷하게 동작한다고 추론할 수 있다.