Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[서버 사이드 렌더링 - 1단계] 해리(최현웅) 미션 제출합니다. #19

Merged
merged 11 commits into from
Oct 11, 2024

Conversation

hwinkr
Copy link

@hwinkr hwinkr commented Oct 9, 2024

안녕하세요 초코~ 반갑습니다 :) 잘 부탁드려요!

👀 리뷰를 통한 생각 나눔

SSR 렌더링 시 초기 렌더링 성능이 왜 유리할까?

💡 작성 요령

서버가 요청을 받은 순간부터 브라우저에 최종 HTML을 반환할 때까지 어떤 단계가 있는지 파악해야 합니다.
도식화해서 요청과 응답의 흐름을 파악하면 구체적으로 렌더링 방식에 관해 이해할 수 있어요.

이번 미션에서 서버가 요청을 받은 순간부터 브라우저에 최종 HTML을 반환할 때까지 어떤 단계가 있는지를 파악해 보면 다음과 같습니다.

사용자가 홈 페이지에 들어온다. -> 서버에 홈 페이지를 그리기 위한 리소스를 요청한다 -> 서버는 영화 정보 요청 api를 호출한다 -> TMBD 데이터베이스에서 영화 정보를 전달해 준다. -> 서버에서는 받은 영화 정보 데이터를 바탕으로 클라이언트에 전달해 주기 위한 HTML 마크업을 구성한다 -> 클라이언트는 서버로부터 받은 HTML을 파싱하고 브라우저 렌더링 과정을 거친 후 사용자는 최종 렌더링 결과물을 확인하게 된다.

서버에서 렌더링한 영화 목록을 어떻게 클라이언트에 데이터를 전달하고 브라우저에서는 어떤 작업을 수행할까?

💡 작성 요령

서버 측에서 클라이언트 측 컴포넌트 코드를 불러올 때 리액트에서 제공하는 어떤 함수를 사용하고 있는지 파악해 보세요.
브라우저에서 서버로부터 HTML을 받고 난 뒤, 리액트 앱이 로드되면 가장 먼저 어떤 작업을 수행할지와 그 이유에 관해 고민해 보세요.

서버 측에서 클라이언트(리액트) 측 컴포넌트 코드를 불러올 때, 리액트에서 제공해 주는 renderToString 함수를 호출합니다. 이 함수의 역할은 다음과 같습니다. (참고: https://ko.react.dev/reference/react-dom/server/renderToString)

  • renderToString은 React 트리를 HTML 문자열로 렌더링합니다.
  • 서버에서 renderToString을 실행하면 앱을 HTML로 렌더링합니다. 이 후, 클라이언트에서 hydrateRoot을 호출하면 서버에서 생성된 HTML을 상호작용하게 만듭니다.

즉, 서버에서 클라이언트 측 컴포넌트를 문자열로 렌더링 하고, 문자열로 변경된 리액트 컴포넌트 구조에 데이터를 채운 뒤 클라이언트에 전달해 주게 됩니다.

router.use(MOVIE_PAGE_PATH.home, async (req, res) => {
  const movies = await fetchMovies(MOVIE_TYPE_KEY[req.path]);

  const renderedApp = renderToString(<App movies={movies} />); // 클라이언트 측 App 컴포넌트를 렌더링
  const templatePath = path.resolve(__dirname, "index.html");
  const template = fs.readFileSync(templatePath, "utf8");

  res.send(
    template
      .replace('<div id="root"></div>', `<div id="root">${renderedApp}</div>`)
      //...
      )
  );
});

서버가 기본적으로 가지고 있던 html + 채워진 마크업을 res 객체의 send 메서드를 활용해서 클라이언트에 보내주게 됩니다.

이 후 브라우저에서는 HTML을 받고난 후, 가장 먼저 앱을 인터렉티브하게 만들기 위한 하이드레이션 과정을 거칩니다. 그 이유는, 서버에서 만들어진 최초의 HTML은 사용자와 상호작용할 수 없는 정적인 페이지이기 때문입니다. 리액트 앱은 사용자와의 상호작용을 위한 로직과 그에 대한 상태 변경 로직을 가지고 있을 것인데, 서버에서 전달해주는 최초의 HTML은 해당 로직이 없기 때문에 하이드레이션 과정부터 가장 먼저 거치게 될 것 같습니다.

hydration을 해석해 보면, 수분공급이라는 뜻인데요. 아마 정적인 HTML 페이지에 인터렉티브라는 수분을 공급해 주기 때문에 해당 단어를 사용하게 된 것 같아요. :)

실제로 미션을 구현할 때,

<button
  className="primary detail"
  onClick={() => alert("Harry Mission Completed")}
></button>;

버튼을 클릭하면 alert 함수를 호출하도록 했는데, hydration이 완료되지 않는다면 버튼을 클릭해도 alert 창이 나타나지 않더군요!

전역 객체 초기화 작업은 왜 진행해야 할까?

💡 작성 요령

아래와 같은 오류가 왜 발생하는지 탐구해 보세요.

react-dom.development.js:86 Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>
react-dom.development.js:12507 Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server.
/client/main.js

import React from "react";
import { hydrateRoot } from "react-dom/client";
import App from "./App";

const { movies } = window.__INITIAL_DATA__;

hydrateRoot(document.getElementById("root"), <App movies={movies} />);

클라이언트 측에서 전역 객체를 초기화해 주지 않는다면,
image
해당 이미지와 같은 하이드레이션 에러가 발생하는 것을 확인할 수 있었습니다. 해당 에러가 발생한 이유는 리액트 공식문서에 잘 설명이 되어있더군요! (참고: https://ko.react.dev/reference/react-dom/client/hydrateRoot#hydrating-server-rendered-html)

hydrateRoot에 전달한 React 트리는 서버에서 만들었던 React 트리 결과물과 동일해야 합니다.

이는 사용자 경험을 위해서 중요합니다. 사용자는 서버에서 만들어진 HTML을 JavaScript 코드가 로드될 때까지 둘러보게 됩니다. 앱의 로딩을 더 빠르게 하기 위해 서버는 일종의 신기루로서 React 결과물인 HTML 스냅샷을 만들어 보여줍니다. 갑자기 다른 컨텐츠를 보여주게 되면 신기루가 깨져버리게 됩니다. 이런 이유로 서버에서 렌더링한 결과물과 클라이언트에서 최초로 렌더링한 결과물이 같아야 합니다.

주로 아래와 같은 원인으로 hydration 에러가 일어납니다.

- React를 통해 만들어진 HTML의 root node안에 새 줄같은 추가적인 공백.
- typeof window !== 'undefined'과 같은 조건을 렌더링 로직에서 사용함.
- window.matchMedia같은 브라우저에서만 사용가능한 API를 렌더링 로직에 사용함.
- 서버와 클라이언트에서 서로 다른 데이터를 렌더링함.

React는 hydration 에러에서 복구됩니다, 하지만 다른 버그들과 같이 반드시 고쳐줘야 합니다. 가장 나은 경우는 그저 느려지기만 할 뿐이지만, 최악의 경우엔 이벤트 핸들러가 다른 엘리먼트에 붙어버립니다.

@hwinkr hwinkr requested a review from 00kang October 9, 2024 09:40
@hwinkr hwinkr self-assigned this Oct 9, 2024
Copy link
Member

@00kang 00kang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해리 안녕하세용

클론받아서 hydration까지 잘 작동하는 것도 확인했고, 코드 작업 부분도 뼈대 코드 적용, 컴포넌트 분리까지 저랑 거의 비슷하게 하셔서 특별히 논의해야할 부분이 딱히 보이지 않는 것 같아요. :) PR도 깔끔하게 정리해주셔서 더 그런 것 같네요 ㅎ
일단 approve드리나, 혹시 논의하고 싶은 부분이 있다면 남겨주세요!
이후 재리뷰요청해주시면 바~로 merge까지 해드리겠습니다!

Copy link
Member

@00kang 00kang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이전 코멘트가 슬랙 알림이 가지 않은 것 같아 코드리뷰와는 별개로 질문 하나 드리고자 합니다 !
Q. 혹시 해리만의 CSR, SSR 적용 기준이 있을까요?

@woowahan-cron woowahan-cron added the 🥎 크론 확인 크론이 확인한 경우 레이블을 지정합니다. label Oct 10, 2024
@hwinkr
Copy link
Author

hwinkr commented Oct 11, 2024

이전 코멘트가 슬랙 알림이 가지 않은 것 같아 코드리뷰와는 별개로 질문 하나 드리고자 합니다 !
Q. 혹시 해리만의 CSR, SSR 적용 기준이 있을까요?

저는 지금까지 리액트로만 개발을 해왔어서, CSR 사고에 익숙해져있어요. 레벨4 SSR 미션에서 처음 접해본 키워드가 너무 많아서 두 렌더링 전략을 언제 어떻게 적용할 수 있을지에 대한 저만의 기준은 아직 자리잡지 못한 것 같아요. 그래서 적용 기준은 경험치가 더 쌓이고 나서야 답변 드릴 수 있을 것 같아요...😅

@00kang 00kang merged commit 3d57294 into woowacourse:hwinkr Oct 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🥎 크론 확인 크론이 확인한 경우 레이블을 지정합니다.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants