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단계] 마루(박규한) 미션 제출합니다. #10

Merged
merged 8 commits into from
Oct 11, 2024

Conversation

rbgksqkr
Copy link

@rbgksqkr rbgksqkr commented Oct 8, 2024

안녕하세요 다르~! 서버에서 리액트 앱을 다룬다는 게 어색해서 조금 어려웠습니다!! 다르의 가감없는 피드백 주면 적극적으로 소통하겠습니다 🔥🔥🔥🔥

👀 리뷰를 통한 생각 나눔

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

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

초기 렌더링 성능은 유의미한 컨텐츠 페이지가 얼마나 빨리 뜨는지를 기준으로 하였습니다.
CSR : 빈 HTML을 불러온 후, 자바스크립트를 실행하여 API를 호출하고 이벤트 리스너가 다 달린 DOM 트리를 그림
SSR : API 서버에서 받은 데이터가 HTML에 채워져서 불러와진 후 자바스크립트를 실행하여 컴포넌트 로직 수행
따라서, SSR 방식으로 렌더링할 경우 DOM을 찾아 이벤트를 등록하는 작업과 사용자의 네트워크 환경에 따라 API 응답 속도에 영향을 받는 작업을 줄였다고 생각합니다. HTML을 그린 후 수행하기 때문에 초기 렌더링 성능에 유리하다고 할 수 있습니다.

요청과 응답의 흐름 파악

  1. 브라우저가 SSR 서버에 HTML을 요청한다.
  2. SSR 서버의 라우터에서 요청을 받는다.
  3. SSR 서버에서 API 서버에 데이터를 요청한다.
  4. 데이터를 추가한 React 컴포넌트의 HTML 문자열을 생성하고, 해당 HTML 문자열을 포함한 HTML을 브라우저에 응답한다. (HTML에는 이벤트같은 상호작용이 없는 상태)
  5. 클라이언트는 hydrateRoot를 실행하여 서버에서 생성된 HTML에 상호작용을 추가한다.
image
  1. 서버에서 렌더링한 영화 목록을 어떻게 클라이언트에 데이터를 전달하고 브라우저에서는 어떤 작업을 수행할까?

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

서버 측에서 클라이언트 측 컴포넌트 코드를 불러올 때는 renderToString 함수를 통해 React 트리를 HTML 문자열로 렌더링합니다.
그리고 서버에서와 클라이언트에서 같은 DOM 계층 구조를 가져가기 위해 API 응답값을 최상단 리액트 컴포넌트에 주입합니다.
브라우저가 서버로부터 HTML을 받은 후, 리액트 앱이 로드되면 트리를 비교하며 이벤트 리스너들을 추가하여 동적인 웹이 완성됩니다.

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

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

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.

SSR 서버에서 렌더링된 HTML과 브라우저에서 렌더링된 가상돔 트리 간에 차이가 있어 에러가 발생합니다.
두 트리가 같은 트리를 그리기 위해 스크립트 태그로 전역 객체에 데이터를 주입합니다.
전역 객체를 초기화하여 데이터를 넘기는 이유는 HTML을 불러오고 리액트 앱을 처음 그릴 때부터 데이터에 접근할 수 있어야하기 때문이라고 생각합니다.

질문

서버에서 브라우저로 HTML을 넘길 때 window 객체를 사용했는데, 뭔가 사용하면서도 전역 객체로 데이터를 전달하는 게 어색하더라구요! 그래서 이거에 대해서 어떻게 생각했는지도 궁금합니다!

+추가

hydration이 제대로 적용되는 줄 알았는데 이전 코드에서는 클라이언트 코드를 아예 불러오지 않은 상태더라고요! 확인해보니 스크립트도 불러오지 않아서 꽤나 삽질을 했습니다 ㅎㅎ 크론이 추가해준 뼈대 코드 반영해서 hydration 적용되어 이벤트 발생하는 것까지 확인하였습니다!

default.mov

@rbgksqkr rbgksqkr self-assigned this Oct 8, 2024
Copy link

@pp449 pp449 left a comment

Choose a reason for hiding this comment

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

안녕하세요 마루!

정말 열심히 코드를 읽어봤는데 군더더기 없이 깔끔해서 지적할 부분이 없네요.. ㅎㅎ
단순히 의견을 물어보기 위해 RC 날려봅니다~

SSR 속도

SSR 방식으로 렌더링할 경우 DOM을 찾아 이벤트를 등록하는 작업과 사용자의 네트워크 환경에 따라 API 응답 속도에 영향을 받는 작업을 줄였다고 생각합니다. HTML을 그린 후 수행하기 때문에 초기 렌더링 성능에 유리하다고 할 수 있습니다.

1

여기서 사용자의 네트워크 환경에 따라 API 응답 속도에 영향을 받는 작업을 줄였다고 말해줬는데, 자체 서버의 응답을 처리한게 아닌 TMDB 라는 외부 서버에 데이터를 요청하여 응답값을 기반으로 HTML 파싱을 하기에 API 응답 속도 차이가 없을거라는 생각도 드는데 요 부분에 대해서 조금 더 자세히 설명해줄 수 있을까요 ?.?

2

서버에서 브라우저로 HTML을 넘길 때 window 객체를 사용했는데, 뭔가 사용하면서도 전역 객체로 데이터를 전달하는 게 어색하더라구요! 그래서 이거에 대해서 어떻게 생각했는지도 궁금합니다!

저도 처음에는 완성된 HTML을 반환한다면 그대로 사용하면 되는데 왜 window 객체에 값을 초기화하며 넘기는지 이해를 못했었는데, 제가 미션을 진행하며 느낀점은

  1. 서버에서 전송한 HTML 과 React의 SPA 로 그려내는 DOM 이 일치한 DOM TREE 를 가지고 있는지 검증하기 위해
  2. 서버에서 패칭한 데이터를 클라이언트 사이드에서 다시 패칭하여 렌더링이 할때

개인적으로 이번 미션을 Nextjs 의 동작과 비교하면서 진행해보니 재밌는게 많더라구요!


hydrateRoot(
document.getElementById("root"),
<App popularMovies={data.movies} bestMovieItem={data.movies[0]} />
Copy link

Choose a reason for hiding this comment

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

개인적으론 App 컴포넌트에 prop 을 넘겨주는 흐름이 일반적이진 않는거 같은데 App 의 prop 으로 값을 넘겨준 이유가 있나요?

Copy link
Author

@rbgksqkr rbgksqkr Oct 10, 2024

Choose a reason for hiding this comment

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

서버측 코드에서 renderToString을 수행할 때를 생각하면, 주입할 리액트 컴포넌트의 최상단 컴포넌트를 넣는 게 유지보수 관점에서 더 적절하다고 생각했어요!

페이지단의 코드가 바뀌는 경우가 종종 있는데, App 내부의 페이지 코드가 renderToString 인자로 들어가게 되면 SSR서버 코드도 변경이 필요하다는 점에서 불편하다고 느꼈습니다.

next에서 SSR로 데이터를 정확히 어떻게 가져오는지는 모르겠지만, 제가 이해한 것으론 App에 주입하는 것과 같이 페이지단에 데이터를 주입한다고 생각했어요!
이에 대한 다르의 생각도 궁금하네용

https://nextjs.org/docs/pages/building-your-application/rendering/server-side-rendering

next ssr

Copy link

Choose a reason for hiding this comment

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

페이지단의 코드가 바뀌는 경우가 종종 있는데, App 내부의 페이지 코드가 renderToString 인자로 들어가게 되면 SSR서버 코드도 변경이 필요하다는 점에서 불편하다고 느꼈습니다.

오호 그렇군용. 저는 개인적으로 App에서부터 데이터가 필요한 곳까지 props drilling 하는게 불필요하고, 흐름이 어색하다고 생각하여 App에 데이터를 넣는게 아닌 필요한 컴포넌트에 데이터를 넣어서 서버에서 렌더링 한 후 완성된 html 파일을 클라이언트에게 반환하도록 했어요!

개인적으로 next 프레임워크와 비교를 해봤을 때, (page router 기준) next에서 _app.tsx 파일과 React의 App 이랑 비슷하다는 생각을 했고

next에서 서버 데이터를 주입하는 위치는 라우팅 페이지의 최상위 컴포넌트였기에(page 디렉토리 하위의 컴포넌트) 여기서 아이디어를 얻어서 App이 아닌 컴포넌트에서 데이터를 주입하도록 했어요

물론 굳이 next를 따라하는게 좋은건 아니지만 위에서 언급한대로 App 에서부터 데이터를 내려주는 흐름이 개인적으론 어색하기도 하고 불필요한 props drilling 인것 같다는 개인적인 의견이에요!

@rbgksqkr
Copy link
Author

rbgksqkr commented Oct 10, 2024

안녕하세요 다르!!! 세세한 질문 감사합니다! 덕분에 더 궁금해져서 실험까지 진행해보았습니다 🔥

  1. 여기서 사용자의 네트워크 환경에 따라 API 응답 속도에 영향을 받는 작업을 줄였다고 말해줬는데, 자체 서버의 응답을 처리한게 아닌 TMDB 라는 외부 서버에 데이터를 요청하여 응답값을 기반으로 HTML 파싱을 하기에 API 응답 속도 차이가 없을거라는 생각도 드는데 요 부분에 대해서 조금 더 자세히 설명해줄 수 있을까요 ?.?

엇 제가 작성한 부분은 사용자 네트워크 환경에 API 요청 속도가 영향을 덜 받는다고 생각했습니다!
브라우저에서 네트워크를 3G로 설정하고 실행했을 때를 예시로 들면,
CSR은 브라우저에서 자바스크립트로 API 서버에 요청을 보내기 때문에 사용자의 네트워크 영향을 받고, SSR은 SSR서버에서 API서버로 요청을 보내기 때문에 사용자의 네트워크 영향을 덜 받는다고 생각했습니다.

근데 실제로 그런지는 다르의 질문을 받고 궁금해서 한번 측정해봤는데요. 외부 API 서버의 상태에 따라 다르고, 코드 크기도 달라 정확하진 않겠지만 사용자 네트워크에 영향을 받는지만 판단하였습니다!

SSR

처음 연결은 별도의 통신 작업이 추가되어 제외한다면,
두번째 연결만 제한 없음으로 설정하고, 나머지는 3G로 설정하여 콘솔을 찍은 결과입니다.
3G 설정이 제한 없음보다 빠를 때도 있는 것을 보면 네트워크 환경에 비례해서 영향을 받지 않는 것을 알 수 있습니다.

image
router.get("/", async (_, res) => {
  console.time();
  const popularMovies = await fetchMoviesPopular();
  console.timeEnd();
   ...
}

CSR

네트워크 쓰로틀링을 3G -> 느린 4G -> 빠른 4G -> 제한 없음 순으로 설정하고 콘솔을 찍은 결과입니다.
2689ms -> 923ms -> 404ms -> 227ms 로 차이가 확연하게 보였습니다.

image
  useEffect(() => {
    const fetchData = async () => {
      console.time();
      await loadMovies(TMDB_MOVIE_LISTS.popular, setPopularMovies);
      console.timeEnd();
    };
    
    fetchData();
   ...
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

=> 측정 결과, "CSR에서 API 응답 속도는 사용자의 네트워크 환경에 영향을 받고, SSR 에서는 영향 받지 않는다." 라고 결론을 내렸습니다 !! 근데 제가 잘못 측정한 걸수도 있으니 다르의 생각도 작성해주시면 감사하겠습니당

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

@pp449 pp449 left a comment

Choose a reason for hiding this comment

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

오오 실험까지 해보느라 고생하셨네요! 💪

해당 실험을 보니 확실히 클라이언트의 요청과 서버의 요청을 본다면 서버의 요청이 훨씬 빠르다는것을 알 수 있네요!

클라이언트에서 보내는 요청과 서버에서 보내는 요청의 차이는 브라우저에 의한 헤더, 쿠키의 데이터가 추가되기에 패킷의 크기가 조금은 클 수 있지만 매우 미비할거다라는 생각을 했는데 생각보다는 차이가 있어서 신기하게 결과를 봤습니다

확실히 클라이언트의 네트워크 환경이 불안정할 수 있으니 서버의 리소스를 사용하는게 더 괜찮을 수 있겠군요 ㅎㅎ (서버가 바쁘지 않는 이상)

이번 미션 고생하셨습니다 다음 미션도 화이팅이이에요 🔥

@pp449 pp449 merged commit 51afa3d into woowacourse:rbgksqkr 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