01
왜 기존 SSR API로는 스트리밍을 할 수 없는가
React에서 서버 렌더링에 오래 쓰인 함수는 renderToString입니다. 이름 그대로 React 트리를 문자열로 바꿉니다.
const html = renderToString(<App />);
그런데 이 함수로는 앞에서 본 스트리밍을 할 수 없습니다. 왜 그런지 직접 알아보겠습니다.
목록을 서버에서 가져와 그리는 컴포넌트를 만듭니다.
async function TodoList() {
// 임의로 2초가 걸리는 getTodoList를 호출합니다.
// 뒤에서도 계속 사용되는 함수여서 2초라는 사실을 기억해주셔야 합니다.
const todoList = await getTodoList();
return <ul>{...}</ul>;
}
await가 있으니 결과가 즉시 나오지 않습니다. React는 이런 컴포넌트를 Suspense로 감싸서 기다리는 동안 보여줄 것을 지정합니다.
<Suspense fallback={<div>loading...</div>}>
<TodoList />
</Suspense>
이것을 renderToString에 넣고 응답을 확인해봤습니다.
<div><h1>Todo List</h1><!--$!--><template data-msg="Switched to client rendering
because the server rendering aborted due to: The server used renderToString which
does not support Suspense..."></template><div>loading...</div><!--/$--></div>
TTFB: 0.005270s / Total: 0.005307s
목록이 없습니다. loading...만 남았습니다.
React가 HTML 안에 이유를 알려주고 있습니다.
서버 렌더링을 중단했고, 클라이언트 렌더링으로 넘겼으며, 그 이유는
renderToString이Suspense를 지원하지 않기 때문이었습니다.
renderToString 함수의 반환 타입에서 정답을 얻을 수 있었습니다.
function renderToString(element): string;
string을 반환합니다. Promise가 아닙니다.
여기서 중요한 것은 각 함수가 무엇을 반환하는가입니다.
Promise를 돌려줍니다.renderToString은 return하는 순간 완성된 문자열을 갖고 있어야 합니다. “2초 뒤에 드리겠다”고 말할 자리가 없습니다.
트리를 순회하다 TodoList를 만나면 이런 상황이 됩니다.
<html> 그림
<h1> 그림
<TodoList> 만남 → 2초 뒤에 결과가 나온다고 한다.
→ 기다려야 하는데 기다릴 방법이 없다.
→ 포기하고 fallback만 넣는다.
TTFB 0.005초, Total 0.005초. 두 값이 사실상 같은 것도 이 때문이었습니다. 2초를 기다려야 하는 컴포넌트가 있는데도 응답은 fallback을 바로 넣어버리기 때문에 0.005초 만에 끝났습니다.
renderToString을 쓰면서 목록을 담으려면, 데이터를 미리 가져와 props로 넘기는 방법밖에 없었습니다.
const todos = await getTodoList();
const html = renderToString(<App todos={todos} />);
getServerSideProps도 데이터가 모두 준비되어야 HTML이 만들어지는 구조입니다. 목록은 들어가지만 TTFB가 2초가 되고, 사용자는 그동안 빈 화면을 보게 됩니다. 시작 페이지에서 본 왼쪽 화면이 이것입니다.
결국 renderToString으로는 빠른 첫 화면과 완성된 HTML 중 하나를 포기해야 했습니다. React가 에러 메시지에서 알려준 대로, Suspense를 지원하는 다른 함수가 필요했습니다. Suspense를 지원하는 다른 함수가 필요했습니다.