Skip to content
Home » 리액트 서버사이드 렌더링 | 서버사이드 렌더링 (개발자라면 상식으로 알고 있어야 하는 개념 정리 ⭐️) 119 개의 가장 정확한 답변

리액트 서버사이드 렌더링 | 서버사이드 렌더링 (개발자라면 상식으로 알고 있어야 하는 개념 정리 ⭐️) 119 개의 가장 정확한 답변

당신은 주제를 찾고 있습니까 “리액트 서버사이드 렌더링 – 서버사이드 렌더링 (개발자라면 상식으로 알고 있어야 하는 개념 정리 ⭐️)“? 다음 카테고리의 웹사이트 Ar.taphoamini.com 에서 귀하의 모든 질문에 답변해 드립니다: https://ar.taphoamini.com/photos. 바로 아래에서 답을 찾을 수 있습니다. 작성자 드림코딩 이(가) 작성한 기사에는 조회수 87,728회 및 좋아요 3,627개 개의 좋아요가 있습니다.

Table of Contents

리액트 서버사이드 렌더링 주제에 대한 동영상 보기

여기에서 이 주제에 대한 비디오를 시청하십시오. 주의 깊게 살펴보고 읽고 있는 내용에 대한 피드백을 제공하세요!

d여기에서 서버사이드 렌더링 (개발자라면 상식으로 알고 있어야 하는 개념 정리 ⭐️) – 리액트 서버사이드 렌더링 주제에 대한 세부정보를 참조하세요

클라이언트 사이드 렌더링(CSR)과 서버사이드 렌더링(SSR) 요즘 많이 들어 보셨죠? 도대체 이 아이들은 무엇인지, 우리가 어떤 점을 고려해서 선택해서 사용해야 하는지도 함께 9분이라는 짧은 영상안에 모든것을 정리해 보도록 할께요.(↙ 자세한 내용)
___________________________
💡 풀스택 개발 로드맵 ⇢ https://academy.dream-coding.com/pages/912e50
🚀 엘리와 더 체계적으로 배우고 싶다면 ⇢ 드림코딩 아카데미:
http://academy.dream-coding.com/
……………………………………………..
✨ 관련된 영상들
웹 최신 트렌드 : https://youtu.be/eJp_WtnZfao
리액트를 배워보고 싶다면: https://youtu.be/bJLfBq9npwQ
……………………………………………..
☀️ 드림코딩 엘리
모든 드림코더분들이 개발자로 성장해 나가고 꿈을 이루는 여정에 함께 할 채널입니다 🙂
❝Don’t forget to code your dream
여러분들의 멋진 꿈을 코딩하세요 ⭐️
……………………………………………..
☀️ 연락
채널 문의 ⇢ [email protected]
아카데미 문의 ⇢ [email protected]
……………………………………………..

🌐 프론트엔드 개발자 되기 입문자편 영상 목록:
https://www.youtube.com/playlist?list=PLv2d7VI9OotQ1F92Jp9Ce7ovHEsuRQB3Y
📒 자바스크립트 기초 강의 영상 목록:
https://www.youtube.com/playlist?list=PLv2d7VI9OotTVOL4QmPfvJWPJvkmv6h-2
💻 개발자라면 누구나 알고 있으면 좋은 지식 💡
https://www.youtube.com/playlist?list=PLv2d7VI9OotSn1ThdDeqvBx8QuRSd01qv
💰 개발자 경력 관리 영상 목록:
https://www.youtube.com/playlist?list=PLv2d7VI9OotSyQ6RPdytiHID5Cmnv_sA3
📄 이력서 작성 방법
https://www.youtube.com/playlist?list=PLv2d7VI9OotTZfvF0s8Vs0gpRtodlTsgg
📈 IT 트렌드
https://www.youtube.com/playlist?list=PLv2d7VI9OotRTfM1zJLQMCLriBpLFg8r-
📷 개발자 브이로그
https://www.youtube.com/playlist?list=PLv2d7VI9OotR1tALnxov7sAUshyBmfYfU
🤗 생산력 향상 팁
https://www.youtube.com/playlist?list=PLv2d7VI9OotQB-9p29xrAnARhuE1Vf88I
……………………………………………..
00:00 소개
00:13 웹의 역사(SPA 시대까지)
01:55 CSR (클라이언트 사이드 렌더링)
04:22 SSR (서버 사이드 렌더링)
05:59 TTV와 TTI 측면에서 한번더 정리
07:33 어떻게 개선할 수 있을까?
08:16 Gatsby
08:54 Next.JS
09:16 정리
……………………………………………..

#프론트엔드#서버사이드렌더링#웹개발

리액트 서버사이드 렌더링 주제에 대한 자세한 내용은 여기를 참조하세요.

[React] 서버 사이드 렌더링(SSR) / 클라이언트 사이드 렌더링 …

CSR은 쉽게 말해서 클라이언트에서 모두 처리하는 것인데, 서버에서 전체 페이지를 한번 렌더링 하여 보여주고 사용자가 요청할 때마다 리소스를 서버에서 …

+ 여기에 더 보기

Source: ctdlog.tistory.com

Date Published: 3/17/2021

View: 5249

[React] 서버 사이드 렌더링 – 나의 개발능력을 키우기 위한 블로그

서버 사이드 렌더링의 이해. UI를 서버에서 렌더링하는 것; CRA로 프로젝트를 생성하고 개발 서버를 실행해 본다. $ yarn create react-app ssr- …

+ 더 읽기

Source: nyeongnyeong.tistory.com

Date Published: 3/18/2021

View: 8750

리액트를 다루는 기술 ( 서버 사이드 렌더링 ) – 1 – velog

서버 사이드 렌더링 : UI를 서버에서 렌더링하는 것을 의미그동안 만든 리액트 프로젝ㅌ는 기본적으로 클라이언트 사이드 렌더링을 하고 있다.

+ 자세한 내용은 여기를 클릭하십시오

Source: velog.io

Date Published: 1/13/2022

View: 6114

2021.08.21 React 서버 사이드 렌더링 – 코린이의 정리노트

그 이후에 자바스크립트가 실행되고 리액트 컴포넌트가 렌더링 되면서 우리에게 보이는 것이다. 서버 사이드 렌더링을 구현하게 되면, 사용자가 웹 서비스 …

+ 더 읽기

Source: korinkorin.tistory.com

Date Published: 9/14/2021

View: 3783

왜 React와 서버 사이드 렌더링인가? – Subicura

React는 서버 사이드 렌더링을 염두에 두고 설계되었습니다. ReactDOMServer.renderToString 함수는 서버 사이드에서 사용하는 렌더링 함수입니다. 이 …

+ 여기에 보기

Source: subicura.com

Date Published: 7/2/2021

View: 5344

react-router :: 3장. 서버사이드 렌더링

서버사이드 렌더링을 통하여 얻을 수 있는 가장 큰 이점은 검색엔진 최적화 입니다. 리액트, 혹은 다른 자바스크립트 라이브러리/프레임워크로 만들어져 …

+ 여기에 자세히 보기

Source: velopert.com

Date Published: 5/10/2022

View: 5318

[ReactJS] 서버사이드 렌더링(SSR)과 클라이언트사이드 렌더링 …

[ReactJS] 서버사이드 렌더링(SSR)과 클라이언트사이드 렌더링(CSR)이란 … [ReactJS] React 기본 코드 구조 및 component 생성 React를 공부하며 …

+ 자세한 내용은 여기를 클릭하십시오

Source: seizemymoment.tistory.com

Date Published: 12/18/2021

View: 8602

React 서버사이드 렌더링 가이드 (번역) – Luffy Blog

React 서버사이드 렌더링 가이드 (번역). 팀 내에서 React SSR (Server Se Rendering)을 도입해보자는 논의가 오가게 되었다.

+ 여기에 보기

Source: kimsangyeon-github-io.vercel.app

Date Published: 2/15/2022

View: 519

리액트를 다루는 기술 [개정판]: 20.1 서버 사이드 렌더링의 이해

서버 사이드 렌더링은 UI를 서버에서 렌더링하는 것을 의미합니다. 앞에서 만든 리액트 프로젝트는 기본적으로 클라이언트 사이드 렌더링을 하고 있습니다.

+ 여기를 클릭

Source: thebook.io

Date Published: 12/1/2022

View: 3918

리액트 서버사이드 렌더링과 컴포넌트 – yceffort

next + react 로 서버사이드 렌더링 환경을 구축하면서 개발을 하고 있었는데 두 가지 문제에 부딪혔었다. 1. window is not defined , SSR 환경에서 …

+ 더 읽기

Source: yceffort.kr

Date Published: 9/27/2021

View: 8016

주제와 관련된 이미지 리액트 서버사이드 렌더링

주제와 관련된 더 많은 사진을 참조하십시오 서버사이드 렌더링 (개발자라면 상식으로 알고 있어야 하는 개념 정리 ⭐️). 댓글에서 더 많은 관련 이미지를 보거나 필요한 경우 더 많은 관련 기사를 볼 수 있습니다.

서버사이드 렌더링 (개발자라면 상식으로 알고 있어야 하는 개념 정리 ⭐️)
서버사이드 렌더링 (개발자라면 상식으로 알고 있어야 하는 개념 정리 ⭐️)

주제에 대한 기사 평가 리액트 서버사이드 렌더링

  • Author: 드림코딩
  • Views: 조회수 87,728회
  • Likes: 좋아요 3,627개
  • Date Published: 2021. 2. 9.
  • Video Url link: https://www.youtube.com/watch?v=iZ9csAfU5Os

[React] 서버 사이드 렌더링(SSR)

https://youtu.be/7mkQi0TlJQo

[React] 서버 사이드 렌더링(SSR) / 클라이언트 사이드 렌더링(CSR)이란?

노마드코더님의 해당 영상을 보다가 ‘서버 사이드 렌더링(SSR)’이란 말이 계속해서 나와서 한 번 제대로 정리해보고자 글을 작성해보려고 한다.

영상에는 서버 사이드 렌더링(SSR, Server Side Rendering)과 클라이언트 사이드 렌더링(CSR, Client Side Rendering)이라는 2가지 렌더링 방식을 언급한다. 대충 단어만 보면 서버에서 렌더링 하는지, 클라이언트에서 렌더링 하는지의 차이 같은데 정확하게 무슨 차이가 있을까?

일단 공통적으로 들어가는 렌더링이라는 단어부터 정리해보자면 렌더링이란 대체 무엇일까?

한 줄로 말하면, 서버로부터 요청해서 받은 내용을 브라우저 화면에 표시해주는 것이다.

여기서 SSR과 CSR의 차이는 표시해줄 화면을 어디서, 어떻게 그리냐의 차이이다.

CSR은 페이지의 내용을 브라우저에서, 그리고 SSR은 서버에서 페이지의 내용을 다 그려서 브라우저로 던져준다.

그렇다면 CSR, SSR이 무엇을 말하는 것인지는 알았으니 어떻게 작동하는지와 장단점을 자세하게 살펴보자.

1) 클라이언트 사이드 렌더링(CSR)

CSR은 SPA 트렌드와 CPU 성능 상승 + JS 표준화(리액트, 뷰, 앵귤러 등의 프레임워크 발전)와 함께 본격적으로 시작되었다. CSR은 쉽게 말해서 클라이언트에서 모두 처리하는 것인데, 서버에서 전체 페이지를 한번 렌더링 하여 보여주고 사용자가 요청할 때마다 리소스를 서버에서 제공받아 클라이언트가 해석하고 렌더링 하는 방식이다. 서버 사이드 렌더링과 달리 서버에 HTML 문서를 요청하는 것이 아니라, 브라우저에서 자바스크립트로 콘텐츠를 렌더링 하는 것이다.

*SPA (Single Page Application) : 최초 한 번 페이지 전체를 로딩한 뒤, 데이터만 변경하여 사용할 수 있는 애플리케이션

CSR의 간단한 예제를 살펴보면, body 안에는 id=”root”만 달랑 하나 들어있고, 어플리케이션에 필요한 자바스크립트의 링크만 들어가있다.

HTML이 텅텅 비어있기 때문에 처음 접속하게 되면 빈 화면만 보이게 되고, 링크된 자바스크립트를 다운로드 받게 된다.여기에는 어플리케이션에 필요한 로직, 구동하기 위한 프레임워크, 라이브러리의 소스코드들도 모두 포함되어 있다. 그렇기 때문에 처음 다운로드 받을 때 꽤나 시간이 소요될 수 있다. 또한, 앞서 말했듯이 추가적으로 데이터가 필요하면 서버로부터 데이터를 받아와서 클라이언트 쪽에서 자바스크립트와 함께 동적으로 화면을 구성하여 사용자에게 최종적으로 화면을 보여주게 되는 것이다.

CSR의 단점으로는 1) 사용자가 첫 화면을 보기까지의 시간이 오래 걸릴 수 있다는 점과 썩 좋지 않은 2) SEO(Search Engine Optimization)를 꼽을 수 있다.

*여기서 SEO란?

구글과 네이버와 같은 검색 엔진들은 서버에 등록된 웹사이트를 돌아다니면서 웹사이트의 HTML 문서를 분석해서 우리가 검색할 때 웹사이트를 빠르게 검색할 수 있도록 도와준다. 하지만 CSR에서 사용되어지는 HTML의 바디는 앞 선 예제처럼 대부분 텅텅 비어있기 때문에 검색엔진들이 CSR로 작성된 웹페이지를 분석하는데 많은 어려움을 겪고 있다.

이러한 문제점들 때문에, 서버 사이드 렌더링(SSR)이 도입되게 된다.

2) 서버 사이드 렌더링(SSR)

SSR은 클라이언트에서 모든 것을 처리하지 않고, 웹 사이트에 접속하면 서버에서 필요한 데이터를 모두 가져와서 HTML 파일을 만들게 되고, 만들어진 HTML과 HTML 파일을 동적으로 조금 제어할 수 있는 소스코드와 함께 클라이언트에게 보낸다. 클라이언트는 잘 만들어진 HTML 문서를 사용자에게 바로 보여주게 된다.

그렇기 때문에 장점으로 페이지 로딩이 빨라지게 되고, CSR과 달리 모든 컨텐츠가 HTML에 담겨있기 때문에 효율적인 SEO가 가능하다.

SSR에서는, 앞서 CSR의 단점인 1) 느린 페이지 로딩과 2) 좋지 않은 SEO 성능을 해결한 것이다.

이런 SSR 방식에는 그럼 단점이 없을까? 아니다.

1) Blinking Issue

2) 서버 과부하

3) TTV와 TVI의 공백시간

SSR에는 첫번째로 Blinking Issue가 존재하는데, 사용자가 새로고침을 하게 되면 전체 웹사이트를 다시 서버에서 받아와야 하기 때문에 화면이 없어졌다가 나타난다. UX 관점에서 봤을 때 썩 좋지 않다. 두번째로는, 서버에 과부하가 걸리기 쉽다는 점이다. 사용자가 많은 제품일수록 서버에 데이터를 요청하는 횟수가 많아지기 때문이다.

세번째가 치명적인 단점인데, 세번째 단점을 이해하기 위해서는 먼저 TTV(Time To View)와 TTI(Time To Interact)라는 것을 알아야 한다.

TTV와 TTI에 대하여 쉽게 이해할 수 있는 그림이 있어서 가져와봤다.

SSR 방식에서는 서버에서 만들어진 HTML 파일을 가져오게 되고 사용자는 바로 웹 사이트를 볼 수 있다. 하지만 웹 사이트를 동적으로 제어할 수 있는 자바스크립트 파일은 아직 받아오지 않았기 때문에 사용자가 클릭을 해도 아무런 것도 처리할 수가 없는 상태가 된다. 최종적으로 자바스크립트 파일을 받아와야지만 사용자가 원하는 것을 처리할 수 있는 인터랙션이 가능해진다.

그래서 SSR은 사용자가 사이트를 볼 수 있는 시간(TTV)와 실제로 인터랙션이 가능한 시간(TTI)의 공백시간이 꽤 길다는 단점이 존재한다.

반대로 CSR은 HTML만 받아왔을 때는 아무것도 보여지지 않고 링크되어 있는 모든 로직을 처리하는 자바스크립트 파일을 받아오게 되면 웹 사이트가 보여지는 동시에 인터랙션이 가능해진다. TTV와 TTI의 공백시간이 없는 것이다.

좋은 CSR, SSR 개발자가 되기 위해서는..?

CSR을 잘 사용하기 위해서는 최종적으로 번들링 해서 사용자에게 보내주는 자바스크립트 파일을 어떻게 하면 효율적으로 많이 분할해서 첫 번째로 사용자가 사이트를 보기 위해서 필요한 필수적인 요소들만 보낼 수 있을지 고민해봐야 한다.

SSR 같은 경우는 세번째 단점을 보완하기 위해서 TTV와 TTI의 공백시간을 줄이기 위해서 어떤 노력을 할 수 있을지와 어떻게 조금 더 매끄러운 UI와 UX를 제공할 수 있을지를 고민해보면 좋을 것이다.

요즘에는 CSR이나 SSR 말고도 SSG(Static Site Generation)라는 것도 있다고 한다.

Gatsby

React는 CSR에 최적화되어 있는 라이브러리지만 Gatsby라는 라이브러리와 함께 사용하면 리액트로 만든 웹어플리케이션을 정적으로 웹페이지를 미리 생성해두고 서버에 배포할 수 있다.

이렇게 만들어진 웹사이트들은 정적으로 보이지만, 추가적으로 서버에서 데이터를 받아오거나 동적으로 처리해야 되는 로직이 있다면 자바스크립트 파일을 함께 가지고 있을 수 있기 때문에 동적인 요소도 충분히 추가할 수 있다.

그리고 개츠비 다음으로 리액트에서 많이 사용되는 것이 Next.js이다.

Next.JS

Next.js는 강력한 서버 사이드 렌더링을 지원하는 라이브러리였는데, 요즘에는 SSG도 지원하고 CSR과 SSR을 섞어서 더 강력하고 유연하게 목적에 맞게 사용할 수 있도록 지원해준다.

Next.js는 개인적으로 프로젝트를 진행해보면서 공부를 할 생각이다.

그나저나 노마드코더님의 영상 댓글에는 React v18에서는 React만으로도 SSR이 가능해진다는 말이 있는데 그럼 Next.js는 어떻게 되는거지..?

참고 영상

[React] 서버 사이드 렌더링

1. 서버 사이드 렌더링의 이해

UI를 서버에서 렌더링하는 것

CRA로 프로젝트를 생성하고 개발 서버를 실행해 본다.

$ yarn create react-app ssr-recipe

$ cd ssr-recipe

$ yarn start

root 엘리먼트가 비어있다.

즉, 이 페이지는 처음에 빈 페이지

그 이후에 자바스크립트가 실행되고 리액트 컴포넌트가 렌더링되면서 우리에게 보이는 것

서버사이드 렌더링을 구현하면 사용자가 웹 서비스에 방문했을 때 서버 쪽에서 초기 렌더링을 대신해 준다.

그리고 사용자가 html을 전달받을 때 그 내부에 렌더링된 결과물이 보인다.

1.1. 서버 사이드 렌더링의 장점

구글, 네이버, 다음 등의 검색 엔진이 우리가 만든 웹 애플리케이션의 페이지를 원활하게 수집할 수 있다. 리액트로 만든 SPA는 검색 엔진 크롤러 봇처럼 자바스크림트가 실행되지 않는 환경에서는 페이지가 제대로 나타나지 않는다. 따라서 서버에서 클라이언트 대신 렌더링을 해주면 검색 엔진이 페이지의 내용을 제대로 수집해 갈 수 있다. 구글 검색 엔진은 다른 검색 엔진과 달리 검색 엔진에서 자바스크립트를 실행하는 기능이 탑재되어 있으므로 제대로 페이지를 크롤링해 갈 때도 있지만, 모든 페이지에 대해 자바스크립트를 실행해 주지 않는다. 따라서 웹 서비스의 검색 엔진 최적화를 위해서라면 서버 사이드 렌더링을 구현해 주는 것이 좋다.

서버 사이드 렌더링을 통해 초기 렌더링 선능을 개선할 수 있다. 예를 들어 서버 사이드 렌더링이 구현되지 않은 웹 페이지에 사용자가 방문하면, 자바스크립트가 로딩되고 실행될 때까지 사용자는 비어 있는 페이지를 보며 대기해야 한다. 여기에 API까지 호출해야 한다면 사용자의 대기 시간이 더더욱 길어진다. 반명 서버 사이드 렌더링을 구현한 웹 페이지라면 자바스크립트 파일 다운로드가 완료되지 않은 시점에서도 html상에 사용자가 볼 수 있는 콘텐츠가 있기 때문에 대기 시간이 최소화되고, 이로 인해 사용자 경험도 향상된다.

1.2. 서버 사이드 렌더링의 단점

원래 브라우저가 해야 할 일을 서버가 대신 처리하는 것이므로 서버 리소스가 사용된다.

갑자기 수많은 사용자가 동시에 웹페이지에 접속하면 서버에 과부하가 발생할 수 있다.

사용자가 많은 서비스라면 캐싱과 로드 밸런싱을 통해 성능을 최적화해 주어야 한다.

프로젝트 구조가 좀 더 복잡해질 수 있다.

데이터 미리 불러오기, 코드 스플리팅과의 호환 등 고려해야 할 사항이 많아져 개발이 아여워질 수도 있다.

1.3. 서버 사이드 렌더링과 코드 스플리팅 충돌

서버 사이드 렌더링과 코드 스플리팅을 함께 적용하면 작업이 꽤 까다롭다.

별도의 호환 작업 없이 두 기술을 함께 적용하면, 다음과 같은 흐름으로 작동하면서 페이지에 깜빡임이 발생한다. 1. 서버 사이드 렌더링된 결과물이 브라우저에 나타남 2. 자바스크립트 파일 로딩 시작 3. 자바스크립트가 실행되면서 아직 불러오지 않은 컴포넌트를 null로 렌더링함 4. 페이지에서 코드 스플리팅된 컴포넌트들이 사라짐 5. 코드 스플리팅된 컴포넌트들이 로딩된 이루 제대로 나타남

이러한 이슈를 해결하려면 라우트 경로마다 코드 스플리팅된 파일 중에서 필요한 모든 파일을 브라우저에서 렌더링하기 전에 미리 불러와야 한다.

Loadable Components 라이브러리에서 제공하는 기능을 써서 서버사이드 렌더링 후 필요한 파일의 경로를 추출하여 렌더링 결과에 스크립트/스타일 태그를 삽입해 주는 방법으로도 해결 가능하다.

2. 프로젝트 준비하기

서버 사이드 렌더링을 진행하기 전에 리액트 라우터를 사용하여 라우팅하는 간단한 프로젝트를 만들어보자.

$ yarn add react-router-dom

2.1. 컴포넌트 만들기

[components/Red.js]

import React from ‘react’; import ‘./Red.css’; const Red = () => { return

Red

}; export default Red;

[components/Red.css]

.Red { background: red; font-size: 1.5rem; color: white; width: 128px; height: 128px; display: flex; align-items: center; justify-content: center; }

[components/Blue.js]

import React from ‘react’; import ‘./Blue.css’; const Blue = () => { return

Blue

}; export default Blue;

[components/Blue.css]

.Blue { background: blue; font-size: 1.5rem; color: white; width: 128px; height: 128px; display: flex; align-items: center; justify-content: center; }

[components/Menu.js]

import React from ‘react’; import {Link} from ‘react-router-dom’; const Menu = () => { return (

  • Red
  • Blue

); }; export default Menu;

2.2. 페이지 컴포넌트 만들기

[pages/RedPage.js]

import React from ‘react’; import Red from ‘../components/Red’; const RedPage = () => { return ; }; export default RedPage;

[pages/BluePage.js]

import React from ‘react’; import Blue from ‘../components/Blue’; const BluePage = () => { return ; }; export default BluePage;

[App.js]

import React from ‘react’; import {Route} from ‘react-router-dom’; import Menu from ‘./components/Menu’; import RedPage from ‘./pages/RedPage’; import BluePage from ‘./pages/BluePage’; function App() { return (


); }; export default App;

[index.js]

import React from ‘react’; import ReactDOM from ‘react-dom’; import ‘./index.css’; import App from ‘./App’; import reportWebVitals from ‘./reportWebVitals’; import {BrowserRouter} from ‘react-router-dom’; ReactDOM.render( , document.getElementById(‘root’) ); reportWebVitals();

[결과]

3. 서버 사이드 렌더링 구현하기

서버 사이드 렌더링을 구현하려면 웹팩 설정을 커스터마이징해 주어야 한다.

CRA로 만든 프로젝트에서는 웹팩 관련 설정이 기본적으로 모두 숨겨져 있으니 yarn eject명령어를 실행하여 밖으로 꺼내주어야 한다.

$ git add .

$ git commit -m ‘Commit before eject’

$ yarn eject

3.1. 서버 사이드 렌더링용 엔트리 만들기

엔트리는 웹팩에서 프로젝트를 불러올 때 가장 먼저 불러오는 파일 예를들어 현재 작성 중인 리액트 프로젝트에서는 index.js를 엔트리 파일로 사용 이 파일부터 시작하여 내부에 필요한 다른 컴포넌트와 모듈을 불러온다.

서버 사이드 렌더링을 할 때는 서버를 위한 엔트리 파일을 따로 생성해야 한다.

[index.server.js]

import React from ‘react’; import ReactDOMServer from ‘react-dom/server’; const html = ReactDOMServer.renderToString(

Hello Server Side Rendering!

); console.log(html)

기본적인 코드만 작성

서버에서 리액트 컴포넌트를 렌더링할 때는 ReactDOMServer의 renderToString이라는 함수 사용

이 함수에 JSX를 넣어서 호출하면 렌더링 결과를 문자열로 반환한다.

3.2. 서버 사이드 렌더링 전용 웹팩 환경 설정 작성하기

작성한 엔트리 파일을 웹팩으로 불러와서 빌드하려면 서버 전용 환경설정을 만들어주어야 한다.

config/path.js 파일을 열어 스크롤을 맨 아래로 내린 후 module.exports 부분에 다음과 같이 두 줄을 추가한다.

[config/path.js]

(…) module.exports = { dotenv: resolveApp(‘.env’), appPath: resolveApp(‘.’), appBuild: resolveApp(‘build’), appPublic: resolveApp(‘public’), appHtml: resolveApp(‘public/index.html’), appIndexJs: resolveModule(resolveApp, ‘src/index’), appPackageJson: resolveApp(‘package.json’), appSrc: resolveApp(‘src’), appTsConfig: resolveApp(‘tsconfig.json’), appJsConfig: resolveApp(‘jsconfig.json’), yarnLockFile: resolveApp(‘yarn.lock’), testsSetup: resolveModule(resolveApp, ‘src/setupTests’), proxySetup: resolveApp(‘src/setupProxy.js’), appNodeModules: resolveApp(‘node_modules’), swSrc: resolveModule(resolveApp, ‘src/service-worker’), ssrIndexJs: resolveApp(‘src/index.server.js’), // 서버 사이드 렌더링 엔트리 ssrBuild: resolveApp(‘dist’), // 웹팩 처리 후 저장 완료 publicUrlOrPath, }; module.exports.moduleFileExtensions = moduleFileExtensions;

ssrIndexJs는 불러올 파일의 경로

ssrBuild는 웹팩으로 처리한 뒤 결과물을 저장할 경로

다음으로 웹팩 환경 설정 파일을 작성

[config/webpack.config.server.js]

const paths = reuqire(‘./paths’); module.exports = { mode: ‘production’, // 프로덕션 모드로 설정하여 최적화 옵션들을 활성화 entry: paths.ssrIndexJs, // 엔트리 경로 target: ‘node’, // node 환경에서 실행될 것이라는 점을 명시 output: { path: paths.ssrBuild, // 빌드 경로 filename: ‘server.js’, // 파일 이름 chunkFilename: ‘js/[name]/chunk.js’, // 청크 파일 이름 publicPath: paths.publicUrlOrPath, // 정적 파일이 제공될 경로 } };

웹팩 기본 설정

빌드할 때 어떤 파일에서 시작해 파일을 불러오는지, 어디에 결과물을 저장할지 정해준다.

다음으로 로더를 설정한다. 웹팩의 로더는 파일을 불러올 때 확장자에 맞게 필요한 처리를 해준다. 예를들어 자바스크립트는 babel을 사용하여 트랜스파일링 해주고, css는 모든 css코드를 결합해주고, 이미지 파일은 파일을 다른 경로에 따로 저장하고 그 파일에 대한 경로를 자바스크립트에서 참조할 수 있게 해준다.

서버사이드 렌더링을 할 때 css혹은 이미지 파일을 그다지 중요하지 않다. 그렇다고 완정히 무시할 순 없다. 가끔 자바스크립트 내부에서 파일에 대한 경로가 필요하거나 css module처럼 로컬 classname을 참조해야 할 수도 있기 때문 그래서 해당 파일을 로더에서 별도로 설정하여 처리하지만 따로 결과물에 포함되지 않도록 구현할 수 있다.

[config/webpack.config.server.js]

const paths = reuqire(‘./paths’); const getCSSModuleLocalIdent = require(‘react-dev-utils/getCSSModuleLocalIdent’); // CSS Module의 고유 className을 만들 때 필요한 옵션 const cssRegex = /\.css$/; const cssModuleRegex = /\.module\.css$/; const sassRegex = /\.(scss|sass)$/; const sassModuleRegex = /\.module\.(scss|sass)$/; module.exports = { mode: ‘production’, // 프로덕션 모드로 설정하여 최적화 옵션들을 활성화 entry: paths.ssrIndexJs, // 엔트리 경로 target: ‘node’, // node 환경에서 실행될 것이라는 점을 명시 output: { path: paths.ssrBuild, // 빌드 경로 filename: ‘server.js’, // 파일 이름 chunkFilename: ‘js/[name]/chunk.js’, // 청크 파일 이름 publicPath: paths.publicUrlOrPath, // 정적 파일이 제공될 경로 }, module: { rules: [ { oneOf: [ // 자바스크립트를 위한 처리 // 기존 webpack.config.js를 참고하여 작성 { test: /\.(js|mjs|jsx|ts|tsx)$/, include: paths.appSrc, loader: require.resolve(‘babel-loader’), options: { customize: require.resolve( ‘babel-preset-react-app/webpack-overrides’ ), plugins: [ [ require.resolve(‘babel-plugin-named-asset-import’), { loaderMap: { svg: { ReactComponent: ‘@svgr/webpack?-svgo![path]’ } } } ] ], cacheDirectory: true, cacheCompression: false, compact: false } }, // CSS를 위한 처리 { test: cssRegex, exclude: cssModuleRegex, // exportOnlyLocals: true 옵션을 설정해야 실제 CSS 파일을 생성하지 않습니다. loader: require.resolve(‘css-loader’), options: { onlyLocals: true } }, // CSS Module을 위한 처리 { test: cssModuleRegex, loader: require.resolve(‘css-loader’), options: { modules: true, onlyLocals: true, getLocalIdent: getCSSModuleLocalIdent } }, // Sass를 위한 처리 { test: sassRegex, exclude: sassModuleRegex, use: [ { loader: require.resolve(‘css-loader’), options: { onlyLocals: true } }, require.resolve(‘sass-loader’) ] }, // Sass + CSS Module을 위한 처리 { test: sassRegex, exclude: sassModuleRegex, use: [ { loader: require.resolve(‘css-loader’), options: { modules: true, onlyLocals: true, getLocalIdent: getCSSModuleLocalIdent } }, require.resolve(‘sass-loader’) ] }, // url-loader를 위한 설정 { test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], loader: require.resolve(‘url-loader’), options: { emitFile: false, // 파일을 따로 저장하지 않는 옵션 limit: 10000, // 원래는 9.76KB가 넘어가면 파일로 저장하는데 emitFile 값이 false일 때는 경로만 준비하고 파일은 저장하지 않는다. name: ‘static/media/[name].[hash:8].[ext]’ } }, // 위에서 설정된 확장자를 제외한 파일들은 file-loader를 사용한다. { loader: require.resolve(‘file-loader’), exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/], options: { emitFile: false, // 파일을 따로 저장하지 않는 옵션 name: ‘static/media/[name].[hash:8].[ext]’ } } ] } ] }, resolve: { modules: [‘node-modules’] } };

react, react-dom/server 같은 라이브러리를 import 구문으로 불러오면 node_modules에서 찾아 사용한다.

라이브러리를 불러오면 빌드할 때 결과물 파일 안에 해당 라이브러리 관련 코드가 함께 번들링 된다.

브라우저에서 사용할 때는 결과물 파일에 리액트 라이브러리와 우리의 애플리케이션에 관한 코드가 공존해야 하는데, 서버에서는 굳이 결과물 파일 안에 리액트 라이브러리가 들어 있지 않아도 된다. node_modules를 통해 바로 불러와서 사용할 수 있기 때문에

따라서 서버를 번들링할 때는 node_modules에서 불러오는 것을 제외하고 번들링하는 것이 좋다.

이를 위해 webpack-node-externals 라는 라이브러리를 사용해야 한다.

$ yarn add webpack-node-externals

이 라이브러리를 상단에 불러와서 설정에 적용

[config/webpack.config.server.js]

const nodeExternals = require(‘webpack-node-externals’) (…) module.exports = { (…) resolve: { modules: [‘node-modules’] }, externals: [nodeExternals()] };

마지막으로 환경변수를 주입하겠다.

[config/webpack.config.server.js]

const nodeExternals = require(‘webpack-node-externals’) const paths = reuqire(‘./paths’); const getCSSModuleLocalIdent = require(‘react-dev-utils/getCSSModuleLocalIdent’); // CSS Module의 고유 className을 만들 때 필요한 옵션 const webpack = require(‘webpack’); const getClientEnvironment = require(‘./env’); const cssRegex = /\.css$/; const cssModuleRegex = /\.module\.css$/; const sassRegex = /\.(scss|sass)$/; const sassModuleRegex = /\.module\.(scss|sass)$/; const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1)); module.exports = { (…) externals: [nodeExternals()], plugins: [ new webpack.DefinePlugin(env.stringified) // 환경변수를 주입해 준다. ] };

환경변수를 주입하면, 프로젝트 내에서 process.env.NODE_ENV 값을 참조하여 현재 개발 환경인지 아닌지를 알 수 있다.

3.3. 빌드 스크립트 작성하기

방금 만든 환경 설정을 사용하여 웹팩으로 프로젝트를 빌드하는 스크립트를 작성해 보겠다.

scripts 경로를 열어 보면 build.js라는 파일이 있다.

이 스크립트는 클라이언트에서 사용할 빌드 파일을 만드는 작업을 한다.

이 스크립트와 비슷한 형식으로 서버에서 사용할 빌드 파일을 만드는 build.server.js 스크립트를 작성해 본다.

[scripts/build.server.js]

process.env.BABEL_ENV = ‘production’; process.env.NODE_ENV = ‘production’; process.on(‘unhandledRejection’, err => { throw err; }); require(‘../config/env’); const fs = require(‘fs-extra’); const webpack = require(‘webpack’); const config = require(‘../config/webpack.config.server’); const paths = require(‘../config/paths’); function build() { console.log(‘Creating server build’); fs.emptyDirSync(paths.ssrBuild); let compiler = webpack(config); return new Promise((resolve, reject) => { compiler.run((err, status) => { if (err) { console.log(err); return; } console.log(status.toString()); }); }); } build();

코드를 다 작성한 뒤에 다음 명령어를 실행하여 빌드가 잘되는지 확인해 보자

$ node scripts/build.server.js

[결과]

이어서 다음 명령어를 실행하여 작성한 결과물이 잘 작동하는지 확인

$ node dist/server.js

[결과]

매번 빌드하고 실행할 때마다 파일 경로를 입력하는 것이 번거로울 수 있으니, package.json에서 스크립트를 생성하여 더 편하게 명령어를 입력할 수 있도록 하자

[package.json] [결과]

3.4. 서버 코드 작성하기

서버 사이드 렌더링을 처리할 서버를 작성해보자

Express라는 Node.js 웹 프레임워크를 사용하여 웹 서버를 만들겠다. 이 과정은 꼭 Express가 아니어도 상관없으며 Koa, Hapi 또는 connect 라이브러리를 사용하면 구현할 수 있다. 여기서 Express를 사용한 이유는 해당 프레임워크가 사용율이 가장 높고, 추후 정적 파일들을 호스팅할 때도 쉽게 구현할 수 있기 때문

$ yarn add express

[index.server.js]

import React from ‘react’; import ReactDOMServer from ‘react-dom/server’; import express from ‘express’; import {StaticRouter} from ‘react-router-dom’; import App from ‘./App’; const app = express(); // 서버 사이드 렌더링을 처리할 핸들러 함수 const serverRender = (req, res, next) => { // 이 함수는 404가 떠야 하는 상황에 404를 띄우지 않고 서버 사이드 렌더링을 해준다. const context = {}; const jsx = ( ); const root = ReactDOMServer.renderToString(jsx); // 렌더링을 하고 res.send(root); // 클라이언트에게 결과물을 응답한다. }; app.use(serverRender); // 5000 포트로 서버를 가동 app.listen(5000, () => { console.log(‘Running on http://localhost:5000’); });

리액트 라우터 안에 들어있는 StaticRouter라는 컴포넌트가 사용되었다. 이 라우터 컴포넌트는 주로 서버 사이드 렌더링 용도로 사용되는 라우터 props로 넣너주는 location값에 따라 라우팅 지금은 req.url이라는 값을 넣어주었다.

StaticRouter에 context라는 props도 넣어주었다. 이 값을 사용하여 나중에 렌더링한 컴포넌트에 따라 HTTP 상태 코드를 설정해 줄 수 있다.

JS 파일과 CSS파일을 웹 페이지에 불러오는 것은 생략하고, 리액트 서버 사이드 렌더링을 통해 만들어진 결과만 보여주도록 하겠다. $ yarn build:server $ yarn start:server

[결과]

서버 사이드 렌더링이 제대로 이루어졌는지 확인하기 위해 Network탭 확인

새로고침 뒤blue의 response를 눌러보면 컴포넌트 렌더링 결과가 문자열로 잘 전달되었다.

3.5. 정적 파일 제공하기

이번에는 Express에 내장되어 있는 static 미들웨어를 사용하여 서버를 통해 build에 있는 JS, CSS 정적 파일들에 접근할 수 있다.

[index.server.js]

import React from ‘react’; import ReactDOMServer from ‘react-dom/server’; import express from ‘express’; import {StaticRouter} from ‘react-router-dom’; import App from ‘./App’; import path from ‘path’; const app = express(); // 서버 사이드 렌더링을 처리할 핸들러 함수 const serverRender = (req, res, next) => { // 이 함수는 404가 떠야 하는 상황에 404를 띄우지 않고 서버 사이드 렌더링을 해준다. const context = {}; const jsx = ( ); const root = ReactDOMServer.renderToString(jsx); // 렌더링을 하고 res.send(root); // 클라이언트에게 결과물을 응답한다. }; const serve = express.static(path.resolve(‘./build’), { index: false // “/” 경로에서 index.html을 보여 주지 않도록 설정 }); app.use(serve); // 순서가 중요하다. serverRender 전에 위치해야 한다. app.use(serverRender); // 5000 포트로 서버를 가동 app.listen(5000, () => { console.log(‘Running on http://localhost:5000’); });

그 다음 JS와 CSS 파일을 불러오도록 html에 코드를 삽입해 주어야 한다.

불러와야 하는 파일 이름은 매번 빌드할 때마다 바뀌기 때문에 빌드하고 나서 만들어지는 asset-manifest.json파일을 참고하여 불러오도록 작성한다.

한번 yarn build 명령어를 실행한 다음, build 디렉터리의 asset-manifest.json을 열어보자

위 코드에서 밑줄이 그어진 파일을 html 내부에 삽입해 주어야 한다.

[index.server.js]

import React from ‘react’; import ReactDOMServer from ‘react-dom/server’; import express from ‘express’; import {StaticRouter} from ‘react-router-dom’; import App from ‘./App’; import path from ‘path’; import fs from ‘fs’; // asset-manifest.json에서 파일 경로들을 조회한다. const manifest = JSON.parse( fs.readFileSync(path.resolve(‘./build/asset-manifest.json’), ‘utf8’) ); const chunks = Object.keys(manifest.files) .filter(key => /chunk\.js$/.exec(key)) // chunk.js로 끝나는 키를 찾아서 .map(key => ``) // 스크립트 태그로 변환하고 .join(”); // 합침 function createPage(root) { return ` . React App

${root}

${chunks} `; } const app = express(); // 서버 사이드 렌더링을 처리할 핸들러 함수 const serverRender = (req, res, next) => { // 이 함수는 404가 떠야 하는 상황에 404를 띄우지 않고 서버 사이드 렌더링을 해준다. const context = {}; const jsx = ( ); const root = ReactDOMServer.renderToString(jsx); // 렌더링을 하고 res.send(createPage(root)); // 클라이언트에게 결과물을 응답한다. }; const serve = express.static(path.resolve(‘./build’), { index: false // “/” 경로에서 index.html을 보여 주지 않도록 설정 }); app.use(serve); // 순서가 중요하다. serverRender 전에 위치해야 한다. app.use(serverRender); // 5000 포트로 서버를 가동 app.listen(5000, () => { console.log(‘Running on http://localhost:5000’); });

[바뀐 코드]

github.com/velopert/learning-react/blob/master/corrections.md#2032-pg-549-550-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8

[결과]

서버 사이드 렌더링이 잘 되었다.

여기서 링크를 눌러 이동할 때는 클라이언트 렌더링이 되어야 한다. 즉, 다른 링크를 클릭하여 다른 페이지로 이동할 때 네트워크 요청이 추가로 발생하지 않아야 한다.

서버사이드 렌더링을 구현하면 이렇게 첫 번째 렌더링은 서버를 통해 하지만, 그 이후에는 브라우저에서 처리한다.

4. 데이터 로딩

서버 사이드 렌더링을 구현할 때 해결하기 매우 까다로운 문제 중 하나

일반적인 브라우저 환경에서는 API를 요청하고 응답을 받아와서 리액트 state 혹은 리덕스 스토어에 넣으면 자동으로 리렌더링하니 큰 걱정 없음

하지만 서버의 경우 문자열 형태로 렌더링하는 것이므로 state나 리덕스 스토어의 상태가 바뀐다고 해서 자동으로 리렌더링되지 않는다.

대신 renderToString함수를 한 번 더 호출해 주어야 한다.

서버에서는 componentDidMount같은 라이프사이클 API도 사용할 수 없다.

redux-thunk 혹은 redux-saga 미들웨어를 사용하여 API를 호출하는 환경에서 서버 사이드 렌더링을 하는 방법을 알아보자

4.1. redux-thunk 코드 준비하기

$ yarn add redux react-redux redux-thunk axios

액션 타입, 액션 생성 함수, 리듀서 코드를 한 파일에 넣어서 관리하는 Ducks 패턴을 사용하여 리덕스 모듈을 작성하겠다.

[modules/users.js]

import axios from ‘axios’; const GET_USERS_PENDING = ‘users/GET_USERS_PENDING’; const GET_USERS_SUCCESS = ‘users/GET_USERS_SUCCESS’; const GET_USERS_FAILURE = ‘users/GET_USERS_FAILURE’; const getUsersPending = () => ({type: GET_USERS_PENDING}); const getUsersSuccess = payload => ({type: GET_USERS_SUCCESS, payload}); const getUsersFailure = payload => ({ type: GET_USERS_FAILURE, error: true, payload }); export const getUsers = () => async dispatch => { try { dispatch(getUsersPending()); const response = await axios.get( ‘https://jsonplaceholder.typicode.com/users’ ); dispatch(getUsersSuccess(response)); } catch (e) { dispatch(getUsersFailure(e)); throw e; } }; const initialState = { users: null, user: null, loading: { users: false, user: false }, error: { users: null, user: null } }; function users(state=initialState, action) { switch (action.type) { case GET_USERS_PENDING: return {…state, loading: {…state.loading, users: true}}; case GET_USERS_SUCCESS: return { …state, loading: {…state.loading, users: false}, users: action.payload.data }; case GET_USERS_FAILURE: return { …state, loading: {…state.loading, users: false}, error: {…state.error, users: action.payload} }; default: return state; } } export default users;

getUsers라는 thunk 함수를 만들고, 이와 관련된 액션 GET_USERS_PENDING, GET_USERS_SUCCESS, GET_USERS_FAILURE를 사용하여 상태 관리를 해준다.

모듈의 상태에는 loading과 error라는 객체가 들어있다.

로딩 상태와 에러 상태를 이렇게 객체로 만든 이유는 추후 redux-saga를 사용한 서버 사이드 렌더링 방법을 연습할 때 단 하나의 사용자 정보를 가져오는 다른 API를 호출할 것이기 때문

즉, 이 모듈에서 관리하는 API는 한 개 이상이므로 loadingUsers, loadingUser와 같이 각 값에 하나하나 이름을 지어 주는 대신에 loading이라는 객체에 넣어 준 것이다.

모듈을 다 작성한 뒤에 루트 리듀서를 만들고, Provider 컴포넌트를 사용하여 프로젝트에 리덕스를 적용하자

https://jsonplaceholder.typicode.com/users 에서 API 응답 값

[modules/index.js]

import {combineReducers} from ‘redux’; import users from ‘./users’; const rootReducer = combineReducers({users}); export default rootReducer;

[src/index.js]

import React from ‘react’; import ReactDOM from ‘react-dom’; import ‘./index.css’; import App from ‘./App’; import reportWebVitals from ‘./reportWebVitals’; import {BrowserRouter} from ‘react-router-dom’; import {createStore, applyMiddleware} from ‘redux’; import {Provider} from ‘react-redux’; import thunk from ‘redux-thunk’; import rootReducer from ‘./modules’; const store = createStore(rootReducer, applyMiddleware(thunk)); ReactDOM.render( , document.getElementById(‘root’) ); reportWebVitals();

4.2. Users, UsersContainer 컴포넌트 준비하기

사용자에 대한 정보를 보여 줄 컴포넌트를 준비하자

[components/Users.js]

import React from ‘react’; import {Link} from ‘react-router-dom’; const Users = ({users}) => { if(!users) return null; // users가 유효하지 않다면 아무것도 보여주지 않음 return (

    {users.map(user => (

  • {user.username}
  • ))}

); }; export default Users;

[containers/UserContainer.js]

import React, {useEffect} from ‘react’; import Users from ‘../components/Users’; import {connect} from ‘react-redux’; import {getUsers} from ‘../modules/users’ const UsersContainer = ({users, getUsers}) => { // 컴포넌트가 마운트되고 나서 호출 useEffect(() => { if (users) return; // users가 이미 유효하다면 요청하지 않음 getUsers(); }, [getUsers, users]); return ; }; export default connect( state => ({ users: state.users.users }), { getUsers } )(UsersContainer);

서버사이드 렌더링을 할 떄 이미 있는 정보를 재요청하지 않게 처리하는 작업이 중요

이 작업을 하지 않으면 서버 사이드 렌더링 후 브라우저에서 페이지를 확인할 때 이미 데이터를 가지고 있음에도 불구하고 불필요한 API를 호출하게 된다. => 트래픽 낭비, 사용자 경험 저하

컴포넌트를 보여 줄 페이지 컴포넌트를 만들고, 라우트 설정을 해보자

[pages/UsersPage.js]

import React from ‘react’; import UsersContainer from ‘../containers/UserContainer’; const UsersPage = () => { return ; }; export default UsersPage;

[App.js]

import React from ‘react’; import {Route} from ‘react-router-dom’; import Menu from ‘./components/Menu’; import RedPage from ‘./pages/RedPage’; import BluePage from ‘./pages/BluePage’; import UsersPage from ‘./pages/UsersPage’; function App() { return (


); }; export default App;

[components/Menu.js]

import React from ‘react’; import {Link} from ‘react-router-dom’; const Menu = () => { return (

  • Red
  • Blue
  • Users

); }; export default Menu;

아직 구현이 끝나지 않았지만, 리액트 개발 서버에서 방금 구현한 데이터 로딩 기능이 잘 작동하는지 확인해보자

[중간 결과]

4.3. PreloadContext 만들기

현재 getUsers 함수는 UsersContainer의 useEffect 부분에서 호출된다. 이를 클래스 형에서 작성했다면 componentDidMount에서 호출 했을 것이다.

서버 사이드 렌더링을 할 때는 useEffect나 componentDidMount에서 설정한 작업이 호출되지 않는다.

렌더링하기 전에 API를 요청한 뒤 스토어에 데이터를 담아야 하는데, 서버 환경에서 이러한 작업을 하려면 클래스형 컴포넌트가 지니고 있는 constructor 메소드를 사용하거나 render 함수 자체에서 처리해야 한다. 그리고 요청이 끝날 때까지 대기했다가 다시 렌더링해 주어야 한다.

우리는 이 작업을 PreloadContext를 만들고, 이를 사용하는 preloader 컴포넌트를 만들어 처리해보겠다.

[lib/PreloadContext.js]

import {createContext, useContext} from ‘react’; // 클라이언트 환경: null // 서버 환경: {done: false, promise: []} const PreloadContext = createContext(null); export default PreloadContext; // resolve는 함수 타입이다. export const Preloader = ({resolve}) => { const preloadContext = useContext(PreloadContext); if (!preloadContext) return null; // context 값이 유효하지 않다면 아무것도 아지 않음 if (preloadContext.done) return null; // 이미 작업이 끝났다면 아무것도 하지 않음 // promises 배열에 프로미스 등록 // 설령 resolve 함수가 프로미스를 반환하지 않더라도, 프로미스 취급을 하기 위해 // Promise.resolve 함수 사용 preloadContext.promises.push(Promise.resolve(resolve())); return null; };

PreloadContext는 서버 사이드 렌더링을 하는 과정에서 처리해야 할 작업들을 실행하고, 만약 기다려야 하는 프로미스가 있다면 프로미스를 수집한다.

모든 프로미스를 수집한 뒤, 수집된 프로미스들이 끝날 때까지 기다렸다가 그다음에 다시 렌더링하면 데이터가 채워진 상태로 컴포넌트들이 나타나게 된다.

Preloader컴포넌트는 resolve하는 함수를 props로 받아 오며, 컴포넌트가 렌더링될 때 서버 환경에서만 resolve함수를 호출해 준다.

UsersContainer에서 한번 사용해보자.

[containers/UserContainer.js]

import React from ‘react’; import Users from ‘../components/Users’; import {connect} from ‘react-redux’; import {getUsers} from ‘../modules/users’ import {Preloader} from ‘../lib/PreloadContext’ const {useEffect} = React; const UsersContainer = ({users, getUsers}) => { // 컴포넌트가 마운트되고 나서 호출 useEffect(() => { if (users) return; // users가 이미 유효하다면 요청하지 않음 getUsers(); }, [getUsers, users]); return( <> ; ); }; export default connect( state => ({ users: state.users.users }), { getUsers } )(UsersContainer);

4.4. 서버에서 리덕스 설정 및 PreloadContext 사용하기

이제 서버에서 이덕스를 설정해준다.

서버에서 리덕스틑 설정하는 것은 브라우저에서 할 때와 비교하여 큰 차이가 없다.

[index.server.js]

import React from ‘react’; import ReactDOMServer from ‘react-dom/server’; import express from ‘express’; import {StaticRouter} from ‘react-router-dom’; import App from ‘./App’; import path from ‘path’; import fs from ‘fs’; import {createStore, applyMiddleware} from ‘redux’; import {Provider} from ‘react-redux’; import thunk from ‘redux-thunk’; import rootReducer from ‘./modules’; // asset-manifest.json에서 파일 경로들을 조회한다. const manifest = JSON.parse( fs.readFileSync(path.resolve(‘./build/asset-manifest.json’), ‘utf8’) ); const chunks = Object.keys(manifest.files) .filter(key => /chunk\.js$/.exec(key)) // chunk.js로 끝나는 키를 찾아서 .map(key => ``) // 스크립트 태그로 변환하고 .join(”); // 합침 function createPage(root) { return ` React App

${root}

${chunks} `; } const app = express(); // 서버 사이드 렌더링을 처리할 핸들러 함수 const serverRender = (req, res, next) => { // 이 함수는 404가 떠야 하는 상황에 404를 띄우지 않고 서버 사이드 렌더링을 해준다. const context = {}; const store = createStore(rootReducer, applyMiddleware(thunk)); const jsx = ( ); const root = ReactDOMServer.renderToString(jsx); // 렌더링을 하고 res.send(createPage(root)); // 클라이언트에게 결과물을 응답한다. }; const serve = express.static(path.resolve(‘./build’), { index: false // “/” 경로에서 index.html을 보여 주지 않도록 설정 }); app.use(serve); // 순서가 중요하다. serverRender 전에 위치해야 한다. app.use(serverRender); // 5000 포트로 서버를 가동 app.listen(5000, () => { console.log(‘Running on http://localhost:5000’); });

여기서 주의할 점은 서버가 실행될 때 스토어를 한 번만 만드는 것이 아니라, 요청이 들어올 때마다 새로운 스토어를 만든다는 것이다.

이제 PreloadContext를 사용하여 프로미스들을 수집하고 기다렸다가 다시 렌더링하는 작업을 수행해 보겠다.

[index.server.js]

import React from ‘react’; import ReactDOMServer from ‘react-dom/server’; import express from ‘express’; import {StaticRouter} from ‘react-router-dom’; import App from ‘./App’; import path from ‘path’; import fs from ‘fs’; import {createStore, applyMiddleware} from ‘redux’; import {Provider} from ‘react-redux’; import thunk from ‘redux-thunk’; import rootReducer from ‘./modules’; import PreloadContext, { Preloader } from ‘./lib/PreloadContext’; // asset-manifest.json에서 파일 경로들을 조회한다. const manifest = JSON.parse( fs.readFileSync(path.resolve(‘./build/asset-manifest.json’), ‘utf8’) ); const chunks = Object.keys(manifest.files) .filter(key => /chunk\.js$/.exec(key)) // chunk.js로 끝나는 키를 찾아서 .map(key => ``) // 스크립트 태그로 변환하고 .join(”); // 합침 function createPage(root) { return ` React App

${root}

${chunks} `; } const app = express(); // 서버 사이드 렌더링을 처리할 핸들러 함수 const serverRender = async (req, res, next) => { // 이 함수는 404가 떠야 하는 상황에 404를 띄우지 않고 서버 사이드 렌더링을 해준다. const context = {}; const store = createStore(rootReducer, applyMiddleware(thunk)); const preloadContext = { done: false, promises: [] }; const jsx = ( ); ReactDOMServer.renderToStaticMarkup(jsx); // renderToStaticMarkup으로 한번 렌더링한다. try { await Promise.all(preloadContext.promises); // 모든 프로미스를 기다린다. } catch (e) { return res.status(500); } preloadContext.done = true; const root = ReactDOMServer.renderToString(jsx); // 렌더링을 한다. res.send(createPage(root)); // 클라이언트에게 결과물을 응답한다. }; const serve = express.static(path.resolve(‘./build’), { index: false // “/” 경로에서 index.html을 보여 주지 않도록 설정 }); app.use(serve); // 순서가 중요하다. serverRender 전에 위치해야 한다. app.use(serverRender); // 5000 포트로 서버를 가동 app.listen(5000, () => { console.log(‘Running on http://localhost:5000’); });

첫 번째 렌더링을 할 때는 renderToString대신 renderToStaticMarkup이라는 함수를 사용했다.

renderToStaticMarkup은 리액트를 사용하여 정적인 페이지를 만들 때 사용한다.

이 함수로 만든 리액트 렌더링 결과물은 클라이언트 쪽에서 HTML DOM 인터랙션을 지원하기 힘들다.

지금 단계에서 renderToString대신 renderToStaticMarkup함수를 사용한 이유는 그저 Preloader로 넣어 주었던 함수를 호출하기 위해서이다. 또 이 함수의 처리 속독가 renderToString보다 좀 더 빠르기 때문이다.

4.5. 스크립트로 스토어 초기 상태 주입하기

지금까지 작성한 코드는 API를 통해 받아 온 데이터를 렌더링하지만, 렌더링하는 과정에서 만들어진 스토어의 상태를 브라우저에서 재사용하지 못하는 상황이다.

서버에서 만들어 준 상태를 브라우저에서 재사용하려면, 현재 스토어 상태를 문자열로 변환한 뒤 스크립트로 주입해 주어야 한다.

[index.server.js]

(…) function createPage(root, stateScript) { return ` React App

${root}

${stateScript} ${chunks} `; } const app = express(); // 서버 사이드 렌더링을 처리할 핸들러 함수 const serverRender = async (req, res, next) => { (…) const root = ReactDOMServer.renderToString(jsx); // 렌더링을 한다. // JSON을 문자열로 변환하고 악성 스크립트가 실행되는 것을 방지하기 위해 <를 치환처리 // https://redux.js.org/recipes/server-rendering#security-considerations const stateString = JSON.stringify(store.getState()).replace(/>/g, ‘\\u003c’); const stateScript = ``; // 리덕스 초기 상태를 스크립트로 주입한다. res.send(createPage(root, stateScript)); // 클라이언트에게 결과물을 응답한다. }; (…)

브라우저에서 상태를 재사용할 때는 다음과 같이 스토어 생성 과정에서 window.__PRELOADED_STATE__를 초깃값으로 사용하면 된다.

[index.js]

import React from ‘react’; import ReactDOM from ‘react-dom’; import ‘./index.css’; import App from ‘./App’; import reportWebVitals from ‘./reportWebVitals’; import {BrowserRouter} from ‘react-router-dom’; import {createStore, applyMiddleware} from ‘redux’; import {Provider} from ‘react-redux’; import thunk from ‘redux-thunk’; import rootReducer from ‘./modules’; const store = createStore( rootReducer, window.__PRELOADED_STATE__, // 이 값을 초기 상태로 사용함 applyMiddleware(thunk) ); ReactDOM.render( , document.getElementById(‘root’) ); reportWebVitals();

$ yarn build

$ yarn build:server

$ yarn start:server

[결과]

4.6. redux-saga 코드 준비하기

리액트를 다루는 기술 ( 서버 사이드 렌더링 ) – 1

20-1. 서버 사이드 렌더링의 이해

서버 사이드 렌더링 : UI를 서버에서 렌더링하는 것을 의미

그동안 만든 리액트 프로젝ㅌ는 기본적으로 클라이언트 사이드 렌더링을 하고 있다.

클라이언트 사이드 렌더링 : UI 렌더링을 브라우저에서 모두 처리하는 것

yarn create react – app ssr – recipe cd ssr – recipe yarn start

20-1-1. 서버 사이드 렌더링 장점

구글, 네이버 등 검색 엔진이 우리가 만든 웹 애플리케이션의 페이지를 원활하게 수집

리액트로 만든 SPA는 자바스크립트가 실행되지 않는 환경에서는 페이지가 제대로 나타나지 않지만 서버에서 클라이언트 대신 렌더링을 해주면 검색 엔진이 페이지의 내용을 제대로 수집해 갈 수 있음

웹 서비스의 검색 엔진 최적화

초기 렌더링 성능을 개선

서버 사이드 렌더링을 구현한 웹 페이지라면 자바스크립트 파일 다운로드가 완료되지 않은 시점에서도 html상에 사용자가 볼 수 있는 콘텐츠가 있기 때문에 대기 시간이 최소화되고, 사용자 경험도 향상

20-1-2. 서버 사이드 렌더링의 단점

원래 브라우저가 해야 할 일을 서버가 대신 처리하는 것이므로 서버 리소스가 사용

수많은 사용자가 동시에 웹 페이지에 접속하면 서버에 과부하가 발생

사용자가 많은 서비스라면 캐싱과 로드 밸런싱을 통해 성능을 최적화 해 줘야 함

프로젝트의 구조가 복잡

데이터 미리 불러오기, 코드 스플리팅과의 호환 등 고려해야 할 사항이 더 많아져서 개발이 어려워질 수도 있음

20-1-3. 서버 사이드 렌더링과 코드 스플리팅 충돌

서버 사이드 렌더링과 코드 스플리팅을 함께 적용하면 작업이 꽤 까다롭다. 별도의 호환 작업엉ㅄ이 두 기술을 함께 적용하면 작동하면서 페이지에 깜박임이 발생한다.

서버 사이드 렌더링된 결과물이 브라우저에 나타남 자바스크립트 파일 로딩 시작 자바스크립트가 실행되면서 아직 불러오지 않은 컴포넌트를 null로 렌더링함 페이지에서 코드 스플리팅된 컴포넌트들이 사라짐 코드 스플리팅된 컴포넌트들이 로딩된 이후 제대로 나타남

이 이슈를 해결하려면 라우트 경로마다 코드 스플리팅된 파일 중에서 필요한 모든 파일을 브라우저에서 렌더링하기 전에 미리 불러와야 함

이 책에서는 위와 같은 문제점을

Loadable Components 라이브러리에서 제공하는 기능을 써서 서버 사이드 렌더링 후 필요한 파일의 경로를 추출하여 렌더링 결과에 스크립트/스타일 태그를 삽인해 주는 방법으로 해결

실습 흐름

프로젝트 준비 서버 사이드 렌더링 구현 데이터 로딩 코드 스플리팅

20-2. 프로젝트 준비

위 CRA로 만든 프로젝트인 ssr-recipe에

yarn add react – router – dom

20-2-1. 컴포넌트 만들기

간단한 컴포넌트 3개 작성

src/components/Red.js

import React from “react” ; import “./Red.css” ; const Red = ( ) => { return < div className = " Red " > RED ; } ; export default Red ;

src/components/Red.css

.Red { background-color : #ff0000 ; font-size : 1.5rem ; color : #fff ; width : 128px ; height : 128px ; display : flex ; align-items : center ; justify-content : center ; }

src/components/Blue.js

import React from “react” ; import “./Blue.css” ; const Blue = ( ) => { return < div className = " Blue " > BLUE ; } ; export default Blue ;

src/components/Blue.css

.Red { background-color : #0000ff ; font-size : 1.5rem ; color : #fff ; width : 128px ; height : 128px ; display : flex ; align-items : center ; justify-content : center ; }

src/components/Menu.js

import React from “react” ; import { Link } from “react-router-dom” ; const Menu = ( ) => { return ( < ul > < li > < Link to = " /red " > Red < li > < Link to = " /blue " > Blue ) ; } ; export default Menu ;

단순하게 빨간, 파란 박스를 보여 주는 컴포넌트와 각 링크로 이동할 수 있게 해 주는 메뉴 컴포넌트를 만들었다. 이 컴포넌트들을 리액트 앱에서 사용

20-2-2. 페이지 컴포넌트 만들기

각 라우트를 위한 페이지 컴포넌트를 작성

page/RedPage.js

import React from “react” ; import Red from “../components/Red” ; const RedPage = ( ) => { return < Red /> ; } ; export default RedPage ;

page/BluePage.js

import React from “react” ; import Blue from “../components/Blue” ; const BluePage = ( ) => { return < Blue /> ; } ; export default BluePage ;

App.js

import React from “react” ; import { Route } from “react-router-dom” ; import Menu from “./components/Menu” ; import RedPage from “./pages/RedPage” ; import BluePage from “./pages/BluePage” ; const App = ( ) => { return ( < div > < Menu /> < hr /> < Route path = " /red " component = { RedPage } /> < Route path = " /blue " component = { BluePage } /> ) ; } ; export default App ;

BrowserRouter를 사용하여 프로젝트에 리액트 라우터를 적용

index.js

import React from “react” ; import ReactDOM from “react-dom” ; import “./index.css” ; import App from “./App” ; import { BrowserRouter } from “react-router-dom” ; ReactDOM . render ( < BrowserRouter > < App /> , document . getElementById ( “root” ) ) ;

20-3. 서버 사이드 렌더링 구현

서버 사이드 렌더링을 구현하려면 웹팩 설정을 커스터마이징해 주어야 한다. CRA로 만든 프로젝트에서는 웹팩 관련 설정이 기본적으로 모두 숨겨져 있어서 yarn eject 명령어를 실행하여 밖으로 꺼내 준다.

git add . git commit – m ‘Commit before eject’ yarn eject

20-3-1. 서버 사이드 렌더링용 엔트리 만들기

엔트리(entry) : 웹팩에서 프로젝트를 불러올 때 가장 먼저 불러오는 파일

현재 작성 중인 리액트 프로젝트에서는 index.js를 엔트리 파일로 사용한다. 이 파일부터 시작하여 내부 필요한 다른 컴포넌트와 모듈을 불러오고 있다.

서버 사이드 렌더링을 할 때는 서버를 위한 엔트리 파일을 따로 생성해야 한다.

src/index.server.js 작성

import React from “react” ; import ReactDOMServer from “react-dom/server” ; const html = ReactDOMServer . renderToString ( < div > Hello Server Side Rendering ! ) ; console . log ( html ) ;

서버에서 리액트 컴포넌트를 렌더링할 때는 ReactDOMServer의 renderToString이라는 함수를 사용

이 함수에 JSX를 넣어 호출하면 렌더링 결과를 문자열로 반환

20-3-2. 서버 사이드 렌더링 전용 웹팩 환경 설정 작성

작성한 엔트리 파일을 웹팩으로 불러와서 빌드하려면 서버 전용 환경 설정을 만들어 주어야 한다.

config 경로의 paths.js 파일을 열어 스크롤 맨 아래로 내린 후 module.exports 부분에 두 줄 추가

swSrc : resolveModule ( resolveApp , ‘src/service-worker’ ) , ** ssrIndexJs : resolveApp ( ‘src/index.server.js’ ) , ssrBuild : resolveApp ( ‘dist’ ) , publicUrlOrPath ,

ssrIndexJs : 불러올 파일의 경로

ssrBuild : 웹팩으로 처리한 뒤 결과물을 저장할 경로

웹팩 환경 설정 파일을 작성

config/webpack.config.server.js

const paths = require ( “./paths” ) ; module . exports = { mode : “production” , entry : paths . ssrIndexJs , target : “node” , output : { path : paths . ssrBuild , filename : “server.js” , chunkFilename : “js/[name].chunk.js” , publicPath : paths . publicUrlOrPath , } , } ;

웹팩 기본 설정을 작성했다. 빌드할 때 어떤 파일에서 시작해 파일들을 불러오는지, 또 어디에 결과물을 저장할지를 정해 줬다.

이제 로더를 설정한다.

웹팩의 로더 : 파일을 불러올 때 확장자에 맞게 필요한 처리를 해줌.

예를 들어 ) 자바스크립트는 babel을 사용하여 트랜스파일링을 해 주고, CSS는 모든 CSS 코드를 결합, 이미지 파일은 파일을 다른 경로에 따로 저장하고 그 파일에 대한 경로를 자바스크립트에서 참조할 수 있게 해 줌

서버 사이드 렌더링을 할 떄 CSS 또는 이미지 파일은 그다지 중요하지 않지만 완전히 무시할 순 없다. 가끔 JS 내부에서 파일에 대한 경로가 필요하거나, CSS Module 처럼 로컬 className을 참조해야 할 수도 있기 떄문. 그래서 해당 파일을 로더에서 별도로 설정하여 처리하지만 따로 결과물에 포함되지 않도록 구현

config/webpack.config.server.js

const paths = require ( “./paths” ) ; const getCSSModuleLocalIdent = require ( “react-dev-utils/getCSSModuleLocalIdent” ) ; const cssRegex = /\.css$/ ; const cssModuleRegex = /\.module\.css$/ ; const sassRegex = /\.(scss|sass)$/ ; const sassModuleRegex = /\.module\.(scss|sass)$/ ; const env = getClientEnvironment ( paths . publicUrlOrPath . slice ( 0 , – 1 ) ) ; module . exports = { mode : “production” , entry : paths . ssrIndexJs , target : “node” , output : { path : paths . ssrBuild , filename : “server.js” , chunkFilename : “js/[name].chunk.js” , publicPath : paths . publicUrlOrPath , } , module : { rules : [ { oneOf : [ { test : /\.(js|mjs|jsx|ts|tsx)$/ , include : paths . appSrc , loader : require . resolve ( “babel-loader” ) , options : { customize : require . resolve ( “babel-preset-react-app/webpack-overrides” ) , presets : [ [ require . resolve ( “babel-preset-react-app” ) , { runtime : “automatic” , } , ] , ] , plugins : [ [ require . resolve ( “babel-plugin-named-asset-import” ) , { loaderMap : { svg : { ReactComponent : “@svgr/webpack?-svgo,+titleProp,+ref![path]” , } , } , } , ] , ] , cacheDirectory : true , cacheCompression : false , compact : false , } , } , { test : cssRegex , exclude : cssModuleRegex , loader : require . resolve ( “css-loader” ) , options : { importLoaders : 1 , modules : { exportOnlyLocals : true , } , } , } , { test : cssModuleRegex , loader : require . resolve ( “css-loader” ) , options : { importLoaders : 1 , modules : { exportOnlyLocals : true , getLocalIdent : getCSSModuleLocalIdent , } , } , } , { test : sassRegex , exclude : sassModuleRegex , use : [ { loader : require . resolve ( “css-loader” ) , options : { importLoaders : 3 , modules : { exportOnlyLocals : true , } , } , } , require . resolve ( “sass-loader” ) , ] , } , { test : sassRegex , exclude : sassModuleRegex , use : [ { loader : require . resolve ( “css-loader” ) , options : { importLoaders : 3 , modules : { exportOnlyLocals : true , getLocalIdent : getCSSModuleLocalIdent , } , } , } , require . resolve ( “sass-loader” ) , ] , } , { test : [ /\.bmp$/ , /\.gif$/ , /\.jpe?g$/ , /\.png$/ ] , loader : require . resolve ( “url-loader” ) , options : { emitFile : false , limit : 10000 , name : “static/media/[name].[hash:8].[ext]” , } , } , { loader : require . resolve ( “file-loader” ) , exclude : [ /\.(js|mjs|jsx|ts|tsx)$/ , /\.html$/ , /\.json$/ ] , options : { emitFile : false , name : “static/media/[name].[hash:8].[ext]” , } , } , ] , } , ] , } , } ;

이제 코드에서 node_modules 내부의 라이브러리르 불러올 수 있게 설정

const paths = require ( “./paths” ) ; const getCSSModuleLocalIdent = require ( “react-dev-utils/getCSSModuleLocalIdent” ) ; const cssRegex = /\.css$/ ; const cssModuleRegex = /\.module\.css$/ ; const sassRegex = /\.(scss|sass)$/ ; const sassModuleRegex = /\.module\.(scss|sass)$/ ; module . exports = { mode : “production” , entry : paths . ssrIndexJs , target : “node” , output : { path : paths . ssrBuild , filename : “server.js” , chunkFilename : “js/[name].chunk.js” , publicPath : paths . publicUrlOrPath , } , module : { rules : [ { oneOf : [ { test : /\.(js|mjs|jsx|ts|tsx)$/ , include : paths . appSrc , loader : require . resolve ( “babel-loader” ) , options : { customize : require . resolve ( “babel-preset-react-app/webpack-overrides” ) , presets : [ [ require . resolve ( “babel-preset-react-app” ) , { runtime : “automatic” , } , ] , ] , plugins : [ [ require . resolve ( “babel-plugin-named-asset-import” ) , { loaderMap : { svg : { ReactComponent : “@svgr/webpack?-svgo,+titleProp,+ref![path]” , } , } , } , ] , ] , cacheDirectory : true , cacheCompression : false , compact : false , } , } , { test : cssRegex , exclude : cssModuleRegex , loader : require . resolve ( “css-loader” ) , options : { importLoaders : 1 , modules : { exportOnlyLocals : true , } , } , } , { test : cssModuleRegex , loader : require . resolve ( “css-loader” ) , options : { importLoaders : 1 , modules : { exportOnlyLocals : true , getLocalIdent : getCSSModuleLocalIdent , } , } , } , { test : sassRegex , exclude : sassModuleRegex , use : [ { loader : require . resolve ( “css-loader” ) , options : { importLoaders : 3 , modules : { exportOnlyLocals : true , } , } , } , require . resolve ( “sass-loader” ) , ] , } , { test : sassRegex , exclude : sassModuleRegex , use : [ { loader : require . resolve ( “css-loader” ) , options : { importLoaders : 3 , modules : { exportOnlyLocals : true , getLocalIdent : getCSSModuleLocalIdent , } , } , } , require . resolve ( “sass-loader” ) , ] , } , { test : [ /\.bmp$/ , /\.gif$/ , /\.jpe?g$/ , /\.png$/ ] , loader : require . resolve ( “url-loader” ) , options : { emitFile : false , limit : 10000 , name : “static/media/[name].[hash:8].[ext]” , } , } , { loader : require . resolve ( “file-loader” ) , exclude : [ /\.(js|mjs|jsx|ts|tsx)$/ , /\.html$/ , /\.json$/ ] , options : { emitFile : false , name : “static/media/[name].[hash:8].[ext]” , } , } , ] , } , ] , } , resolve : { modules : [ “node_modules” ] , } , } ;

이렇게 하면 react , react-dom/server 같은 라이브러리를 import 구문으로 불러오면 node_modules에서 찾아 사용한다. 라이브러리를 불러오면 빌드할 때 결과물 파일 안에 해당 라이브러리 관련 코드가 함꼐 번들링 된다.

브라우저에서 사용할 때는 결과물 파일에 리액트 라이브러리와 애플리케이션에 관한 코드가 공존해야 하는데, 서버에서는 굳이 겨로가물 파일 안에 리액트 라이브러리가 들어 있지 않아도된다. node_modules를 통해 바로 불어와서 사용할 수 있기 때문이다.

따라서 서버를 위해 번들링할 때는 node_modules에 불러오는 것을 제외하고 번들링하는 것이 좋다. 이를 위해 webpack-node-externals라는 라이브러리를 사용해야 한다.

yarn add webpack – node – externals

webpack.config.server.js의 상단에 불어와서 설정을 적용

const nodeExternals = require ( “webpack-node-externals” ) ; const paths = require ( “./paths” ) ; const getCSSModuleLocalIdent = require ( “react-dev-utils/getCSSModuleLocalIdent” ) ; const cssRegex = /\.css$/ ; const cssModuleRegex = /\.module\.css$/ ; const sassRegex = /\.(scss|sass)$/ ; const sassModuleRegex = /\.module\.(scss|sass)$/ ; module . exports = { mode : “production” , entry : paths . ssrIndexJs , target : “node” , output : { path : paths . ssrBuild , filename : “server.js” , chunkFilename : “js/[name].chunk.js” , publicPath : paths . publicUrlOrPath , } , module : { rules : [ { oneOf : [ { test : /\.(js|mjs|jsx|ts|tsx)$/ , include : paths . appSrc , loader : require . resolve ( “babel-loader” ) , options : { customize : require . resolve ( “babel-preset-react-app/webpack-overrides” ) , presets : [ [ require . resolve ( “babel-preset-react-app” ) , { runtime : “automatic” , } , ] , ] , plugins : [ [ require . resolve ( “babel-plugin-named-asset-import” ) , { loaderMap : { svg : { ReactComponent : “@svgr/webpack?-svgo,+titleProp,+ref![path]” , } , } , } , ] , ] , cacheDirectory : true , cacheCompression : false , compact : false , } , } , { test : cssRegex , exclude : cssModuleRegex , loader : require . resolve ( “css-loader” ) , options : { importLoaders : 1 , modules : { exportOnlyLocals : true , } , } , } , { test : cssModuleRegex , loader : require . resolve ( “css-loader” ) , options : { importLoaders : 1 , modules : { exportOnlyLocals : true , getLocalIdent : getCSSModuleLocalIdent , } , } , } , { test : sassRegex , exclude : sassModuleRegex , use : [ { loader : require . resolve ( “css-loader” ) , options : { importLoaders : 3 , modules : { exportOnlyLocals : true , } , } , } , require . resolve ( “sass-loader” ) , ] , } , { test : sassRegex , exclude : sassModuleRegex , use : [ { loader : require . resolve ( “css-loader” ) , options : { importLoaders : 3 , modules : { exportOnlyLocals : true , getLocalIdent : getCSSModuleLocalIdent , } , } , } , require . resolve ( “sass-loader” ) , ] , } , { test : [ /\.bmp$/ , /\.gif$/ , /\.jpe?g$/ , /\.png$/ ] , loader : require . resolve ( “url-loader” ) , options : { emitFile : false , limit : 10000 , name : “static/media/[name].[hash:8].[ext]” , } , } , { loader : require . resolve ( “file-loader” ) , exclude : [ /\.(js|mjs|jsx|ts|tsx)$/ , /\.html$/ , /\.json$/ ] , options : { emitFile : false , name : “static/media/[name].[hash:8].[ext]” , } , } , ] , } , ] , } , resolve : { modules : [ “node_modules” ] , } , externals : [ nodeExternals ( ) ] , } ;

이제 환경 설정 파일은 거이 작성했고, 마지막으로 환경 변수를 주입

const nodeExternals = require ( “webpack-node-externals” ) ; const paths = require ( “./paths” ) ; const getCSSModuleLocalIdent = require ( “react-dev-utils/getCSSModuleLocalIdent” ) ; const webpack = require ( “webpack” ) ; const getClientEnvironment = require ( “./env” ) ; const cssRegex = /\.css$/ ; const cssModuleRegex = /\.module\.css$/ ; const sassRegex = /\.(scss|sass)$/ ; const sassModuleRegex = /\.module\.(scss|sass)$/ ; const env = getClientEnvironment ( paths . publicUrlOrPath . slice ( 0 , – 1 ) ) ; module . exports = { mode : “production” , entry : paths . ssrIndexJs , target : “node” , output : { path : paths . ssrBuild , filename : “server.js” , chunkFilename : “js/[name].chunk.js” , publicPath : paths . publicUrlOrPath , } , module : { rules : [ { oneOf : [ { test : /\.(js|mjs|jsx|ts|tsx)$/ , include : paths . appSrc , loader : require . resolve ( “babel-loader” ) , options : { customize : require . resolve ( “babel-preset-react-app/webpack-overrides” ) , presets : [ [ require . resolve ( “babel-preset-react-app” ) , { runtime : “automatic” , } , ] , ] , plugins : [ [ require . resolve ( “babel-plugin-named-asset-import” ) , { loaderMap : { svg : { ReactComponent : “@svgr/webpack?-svgo,+titleProp,+ref![path]” , } , } , } , ] , ] , cacheDirectory : true , cacheCompression : false , compact : false , } , } , { test : cssRegex , exclude : cssModuleRegex , loader : require . resolve ( “css-loader” ) , options : { importLoaders : 1 , modules : { exportOnlyLocals : true , } , } , } , { test : cssModuleRegex , loader : require . resolve ( “css-loader” ) , options : { importLoaders : 1 , modules : { exportOnlyLocals : true , getLocalIdent : getCSSModuleLocalIdent , } , } , } , { test : sassRegex , exclude : sassModuleRegex , use : [ { loader : require . resolve ( “css-loader” ) , options : { importLoaders : 3 , modules : { exportOnlyLocals : true , } , } , } , require . resolve ( “sass-loader” ) , ] , } , { test : sassRegex , exclude : sassModuleRegex , use : [ { loader : require . resolve ( “css-loader” ) , options : { importLoaders : 3 , modules : { exportOnlyLocals : true , getLocalIdent : getCSSModuleLocalIdent , } , } , } , require . resolve ( “sass-loader” ) , ] , } , { test : [ /\.bmp$/ , /\.gif$/ , /\.jpe?g$/ , /\.png$/ ] , loader : require . resolve ( “url-loader” ) , options : { emitFile : false , limit : 10000 , name : “static/media/[name].[hash:8].[ext]” , } , } , { loader : require . resolve ( “file-loader” ) , exclude : [ /\.(js|mjs|jsx|ts|tsx)$/ , /\.html$/ , /\.json$/ ] , options : { emitFile : false , name : “static/media/[name].[hash:8].[ext]” , } , } , ] , } , ] , } , resolve : { modules : [ “node_modules” ] , } , externals : [ nodeExternals ( { allowlist : [ /@babel/ ] , } ) , ] , } ;

환경 변수를 주입하면, 프로젝트 내에서 process.env.NODE_ENV 값을 참조하여 현재 개발 환경인지 아닌지 알 수 있다.

20-3-3. 빌드 스크립트 작성하기

방금 만든 환경 설정을 사용하여 웹팩으로 프로젝트를 빌드하는 스크립트 작성.

scripts 경로에 build.js라는 파일이 있다. 이 스크립트는 클라이언트에서 사용할 빌드 파일을 만드는 작업을 한다. 이 스크립트와 비슷한 형식으로 서버에서 사용할 빌드 파일을 만드는 build.server.js 스크립트를 작성

process . env . BABEL_ENV = “production” ; process . env . NODE_ENV = “production” ; process . on ( “unhandledRejection” , ( err ) => { throw err ; } ) ; require ( “../config/env” ) ; const fs = require ( “fs-extra” ) ; const webpack = require ( “webpack” ) ; const config = require ( “../config/webpack.config.server” ) ; const paths = require ( “../config/paths” ) ; function build ( ) { console . log ( “Creating server build…” ) ; fs . emptyDirSync ( paths . ssrBuild ) ; let compiler = webpack ( config ) ; return new Promise ( ( resolve , reject ) => { compiler . run ( ( err , stats ) => { if ( err ) { console . log ( err ) ; return ; } console . log ( stats . toString ( ) ) ; } ) ; } ) ; } build ( ) ;

코드를 다 작성했으면

node scripts / build . server . js

성공적으로 잘 실행됐다. 잘 작동하는지 확인하기 위해

node dist / server . js

이거도 잘 출력됐다.

package.json에

“scripts” : { “start” : “node scripts/start.js” , “build” : “node scripts/build.js” , “test” : “node scripts/test.js” , ** “start:server” : “node dist/server.js” , “build:server” : “node scripts/build.server.js” ** } ,

하단 두줄 추가한다.

이렇게 하면

yarn build : server yarn start : server

짧아진 명령어로 실행 할 수 있다.

20-3-4. 서버 코드 작성하기

서버 사이드 렌더링을 처리할 서버를 작성.

Express라는 Node.js 웹 프레임워크를 사용하여 웹 서버를 만든다.

yarn add express

그리고 index.server.js 코드를 작성한다.

import React from “react” ; import ReactDOMServer from “react-dom/server” ; import express from “express” ; import { StaticRouter } from “react-router-dom” ; import App from “./App” ; const app = express ( ) ; const serverRender = ( req , res , next ) => { const context = { } ; const jsx = ( < StaticRouter location = { req . url } context = { context } > < App /> ) ; const root = ReactDOMServer . renderToString ( jsx ) ; res . send ( root ) ; } ; app . use ( serverRender ) ; app . listen ( 5000 , ( ) => { console . log ( “Running on http://localhost:5000” ) ; } ) ;

이 과정에서 리액트 라우터 안에 들어 있는 StaticRouter라는 컴포넌트가 사용됐다.

StaticRouter : 주로 서버 사이드 렌더링 용도로 사용되는 라우터

props로 넣어 주는 location 값에 따라 라우팅해줌 지금은 req.url이라는 값을 넣었는데, 여기서 req 객체는 요청에 대한 정보를 지님

context라는 props도 넣어 주었다. 나중에 렌더링한 컴포넌트에 따라 HTTP 상태 코드를 설정

지금 당장 JS와 CSS 파일을 웹 페이지에 불러오는 것은 생략하고 리액트 서버 사이드 렌더링을 통해 만들어진 결과만 보여 주도록 처리했다.

yarn build : server yarn start : server

지금은 css를 불러오지 ㅇ낳기 때문에 스타일이 적용되지 않아도 괜찮다. 브라우저에서 자바스크립트도 실행되지 않기 때문에, 현재 브라우저에 나타난 정보는 모두 서버 사이드에서 렌더링된 것으로 간주할 수 있다.

자바스크립트를 로딩하면 현재 브라우저에 보이는 데이터가 서버에서 렌더링된 것인지, 클라이언트에서 렌더링된 것인지 분간하기 어려울 것이다. 서버 사이드 렌더링이 정말 제대로 이루어졌는지 확인하기 위해 개발자 도구의 Network 탭을 열고 새로고침을 해본다.

20-3-5. 정적 파일 제공하기

Express에 내장되어 있는 static 미들웨어를 사용하여 서버를 통해 build에 있는 JS, CSS 정적 파일들에 접근할 수 있도록 해 준다.

import React from “react” ; import ReactDOMServer from “react-dom/server” ; import express from “express” ; import { StaticRouter } from “react-router-dom” ; import App from “./App” ; import path from “path” ; const app = express ( ) ; const serverRender = ( req , res , next ) => { const context = { } ; const jsx = ( < StaticRouter location = { req . url } context = { context } > < App /> ) ; const root = ReactDOMServer . renderToString ( jsx ) ; res . send ( root ) ; } ; const serve = express . static ( path . resolve ( “./build” ) , { index : false , } ) ; app . use ( serve ) ; app . use ( serverRender ) ; app . listen ( 5000 , ( ) => { console . log ( “Running on http://localhost:5000” ) ; } ) ;

그 다음 JS와 CSS 파일을 불러오도록 html에 코드를 삽입해 줘야한다. 불러와야 하는 파일 이름은 매번 빌드할 때마다 바뀌기 때문에 빌드하고 나서 만들어지는 asset-manifest.json 파일을 참고하여 불러오도록 작성

한번 yarn build 명령어를 실행한 다음, build 디렉토리에 asset-manifest.json을 본다.

{ “files” : { ** “main.css” ** : “/static/css/main.0be25a32.chunk.css” , ** “main.js” ** : “/static/js/main.4312a39c.chunk.js” , “main.js.map” : “/static/js/main.4312a39c.chunk.js.map” , ** “runtime-main.js” ** : “/static/js/runtime-main.ddba2a55.js” , “runtime-main.js.map” : “/static/js/runtime-main.ddba2a55.js.map” , ** “static/js/2.4332f3ba.chunk.js” ** : “/static/js/2.4332f3ba.chunk.js” , “static/js/2.4332f3ba.chunk.js.map” : “/static/js/2.4332f3ba.chunk.js.map” , “index.html” : “/index.html” , “static/css/main.0be25a32.chunk.css.map” : “/static/css/main.0be25a32.chunk.css.map” , “static/js/2.4332f3ba.chunk.js.LICENSE.txt” : “/static/js/2.4332f3ba.chunk.js.LICENSE.txt” } , “entrypoints” : [ “static/js/runtime-main.ddba2a55.js” , “static/js/2.4332f3ba.chunk.js” , “static/css/main.0be25a32.chunk.css” , “static/js/main.4312a39c.chunk.js” ] }

밑줄 부분을 html 내부에 삽입해 줘야 한다고 한다.

서버 코드를 수정한다.

import React from “react” ; import ReactDOMServer from “react-dom/server” ; import express from “express” ; import { StaticRouter } from “react-router-dom” ; import App from “./App” ; import path from “path” ; import fs from “fs” ; const manifest = JSON . parse ( fs . readFileSync ( path . resolve ( “./build/asset-manifest.json” ) , “utf8” ) ) ; const chunks = Object . keys ( manifest . files ) . filter ( ( key ) => /chunk\.js$/ . exec ( key ) ) . map ( ( key ) => ` ` ) . join ( “” ) ; function createPage ( root ) { return ` React App

${ root }

${ chunks } ` ; } const app = express ( ) ; const serverRender = ( req , res , next ) => { const context = { } ; const jsx = ( < StaticRouter location = { req . url } context = { context } > < App /> ) ; const root = ReactDOMServer . renderToString ( jsx ) ; res . send ( createPage ( root ) ) ; } ; const serve = express . static ( path . resolve ( “./build” ) , { index : false , } ) ; app . use ( serve ) ; app . use ( serverRender ) ; app . listen ( 5000 , ( ) => { console . log ( “Running on http://localhost:5000” ) ; } ) ;

이제 서버를 빌드하고 다시 시작

yarn build : server yarn start : server

http://localhost:5000/red 페이지에 들어가 CSS도 함께 적용되는지 확인하고, 개발자 도구의 Network 탭에서 서버 사이드 렌더링이 잘되었는지 검증해본다. 여기서 링크를 눌러 이동할 때는 클라이언트 렌더링이 되어야 한다. 즉, 다른 링크를 클릭하여 다른 페이지로 이동할 때 네트워크 요청이 추가로 발생하지 않아야 한다고 한다. ( 뭔말인지 잘 모르겠지만..된거라고 생각한다.. )

서버 사이드 렌더링을 구현하면 첫 번쨰 렌더링은 서버를 통해 하지만, 그 이후에는 브라우저에서 처리한다.

2021.08.21 React 서버 사이드 렌더링

2021.08.21 React 서버 사이드 렌더링_ 정리노트

https://korinkorin.tistory.com/70

오늘은 여기서 잠시 정리했던 서버사이드렌더링에 대해서 좀 더 심층적으로 알아보고, 리액트 프로젝트에 구현해볼 예정..!!

그때 몰라서 구글링으로 혼자 찾아보고 간단하게 설레발쳐서 정리했었는데 ㅎㅅㅎ .. 책에도 나오는구나 조아써

그 때 잠시 다뤘던 것처럼 리액트처럼 SPA 에서는 기본적으로 클라이언트 사이드 렌더링을 하고 있다. 얘는 브라우저에서 UI 렌더링을 모두 처리하는데, 자바스크립트를 실행해야, 우리가 만든 화면들이 사용자에게 보인다.

리액트 프로젝트를 만들어서 개발서버를 실행하면 이렇게 네트워크 탭에 localhost 파일이 보임.

얘를 선택해서 Response 를 보면 index.js 파일에 있는 root 엘리먼트가 비어있는게 보일거임. 즉 이 페이지는 처음엔 빈 페이지라는 뜻!

그 이후에 자바스크립트가 실행되고 리액트 컴포넌트가 렌더링 되면서 우리에게 보이는 것이다.

서버 사이드 렌더링을 구현하게 되면, 사용자가 웹 서비스에 방문 했을 때 서버 쪽에서 초기 렌더링을 대신해준다. 그리고 사용자가 html 을 전달받을 때 그 내부에 렌더링된 결과물이 보인다.

그러면 본격적으로 서버사이드렌더링을 구현해보기 전에,

다시 한 번 클라이언트 사이드 렌더링, 서버 사이드 렌더링에 대한 장단점 정리!

클라이언트 사이드 렌더링

첫 요청시 한 페이지만 불러와 사용자의 행동에 따른 필요한 데이터만 다시 읽어들이는 방식.

요즘은 웹에서 제공되는 정보가 정말 많기 때문에 새로고침이 일어나며 페이지를 로딩할 때마다 서버로부터 리소스를 전달받아 해석하고 화면에 렌더링하는 방식인 서버 사이드 렌더링에서 성능 이슈 때문에 나타난 방식.

필요시에 변경된 데이터만 받아서 트래픽을 감소할 수 있고, 새로 고침이 발생하지 않기 때문에 사용자가 네이티브 앱과 비슷한 경험을 할 수 있는 장점이 존재.

하지만 자바스크립트 위주로 돌아가는 프로젝트는 자바스크립트 엔진이 돌아가지 않으면 원하는 정보를 표시해주지 못한다. 위의 화면처럼 첫 렌더링시 root 엘리먼트가 비어있기 때문에, 자바스크립트 엔진이 없는 네이버, 다음 등의 검색 엔진은 이를 제대로 크롤링하지 못한다.

엔진이 있는 구글 또한 제대로 페이지를 크롤링해 갈 때도 있지만, 모든 페이지에 대해 자바스크립트를 실행해주지 않는다.

그리고 앱의 규모가 커지면서 자바스크립트 파일도 같이 커지기 때문에 초기 구동속도가 느리다는 점도 단점이라 할 수 있겠다.

서버사이드 렌더링

전통적인 렌더링 방식.

사용자가 웹 서비스에 방문했을 때 서버 쪽에서 초기 렌더링을 대신해줌. 그러고 나서 사용자가 html 을 전달받을 때 그 내부에 렌더링 된 결과물을 브라우저는 표시해줌.

그렇기 때문에 초기 렌더링 성능이 개선되고, 사용자는 자바스크립트 파일 다운로드가 완료되지 않은 시점에서도 html 이 표시되므로, 대기 시간이 최소화되고, 이로 인한 사용자 경험은 증가하게 된다.

또한 클라이언트 사이드 렌더링과 반대로 검색 엔진을 최적화 할 수 있다.

하지만 결국 브라우저가 해야할 일을 서버가 대신 처리하기 때문에 서버 리소스가 사용되고, 만약 수많은 사용자가 동시에 웹 페이지에 접속하면 서버에 과부하가 발생하게 된다. 따라서 사용자가 많은 서비스라면 캐싱, 로드 밸런싱을 통해 성능을 최적화를 해주어야 한다.

그리고 , 서버 사이드 렌더링을 구현하게 된다면, 프로젝트의 구조가 좀 더 복잡해지고, 데이터 미리 불러오기 코드 스플리팅과의 호환 등 고려해야 할 사항 증가로 개발이 어려워진다는 단점이 있다.

그 중 리액트와 같은 SPA 에서 코드 스플리팅과 서버 사이드 렌더링을 별도의 호환 작업 없이 두 기술을 함께 적용하면, 페이지에 깜빡임 현상이 발생한다.

서버 사이드 렌더링된 결과물이 브라우저에 나타나고, 자바스크립트 파일이 그 다음으로 로딩. 이 때 자바스크립트는 실행하면서 아직 불러오지 않은 컴포넌트를 null 로 렌더링 해버린다.

그러면 렌더링 된 결과물이 갑자기 사라졌다가 다시 코드 스플링이 된 컴포넌트들이 로딩이 되어 나타날 때까지 깜빡거리는 것이다!

그래서 이 문제점을 책에서는 Loadable Component 라이브러리에서 제공하는 기능을 써서 서버 사이드 렌더링 후 필요한 파일의 경로를 추철하여 렌더링 결과에 스크립트/스타일 태그를 삽입해 주는 방법으로 해결한다.

아마 길이가….매우 길 것 같아 서버 사이드 렌더링을 구현하는 포스팅/// 서버 사이드 렌더링과 코드 스플리팅 충돌시 해결 방법 포스팅

으로 나누어서 작성할 예정이다.

일단은 오늘도 프로젝트 생성!

$ yarn create react-app ssr-recipe $ cd ssr-recipe // 서버 사이드 렌더링 구현 프로젝트 준비하기 위해 라우터 설치 $ yarn add react-router-dom

src/components/Red.js

src/components/Bllue.js

(새로 생성)

src/components/Red.css

src/components/Blue.css

(새로 생성)

단순히 빨간색 네모, 파란색 네모 를 생성해주고,

그 다음 각 링크로 이동할 수 있게 해주는 메뉴 컴포넌트도 작성.

src/components/Menu.js ( 새로 생성 )

그리고 페이지 라우트를 위한 페이지 컴포넌트 또한 생성.

src/pages/RedPage.js

src/pagesBluePage.js

( 새로 생성 )

후……. 페이지 컴포넌트도 다 만들었으니 App 컴포넌트에서 라우터만 설정해주면 끝!

src/App

아차차 BrowserRouter 로 리액트 라우터도 적용해야함.

src/index.js

실행 화면

후 이렇게 간단하게 서버 사이드 렌더링을 위한 프로젝트 세팅 끝!

혹시 라우터 부분 흐름이 이해가 안간다면

https://korinkorin.tistory.com/70

굽신 이것도 봐주세여

서버 사이드 렌더링을 구현하려면 웹팩 설정을 커스터마이징 해주어야 함. 근데 CRA 로 만든 프로젝트 에서는 웹팩 관련 설정이 기본적으로 모두 숨겨져있다. 얘를 yarn eject 로 끄집어 내주자.

웹팩

파일을 하나로 합치는 번들러. ( 의존성이 있는 모듈 코드를 하나 또는 여러 개의 파일로 만들어주는 도구)

http 요청의 비효율적 이슈 때문에 사용.

html,js,css,web-font,favicon,image,json data 등등 수많은 파일을 받아와야하는데, http/2 에서는 하나의 커넥션에 동시에 여러 파일들을 요청할 수 있지만, 여전히 파일의 개수가 적어야 좋은 경우들이 존재.

이때 웹팩은 여러 가지 기능을 통해 번들러의 역할을 수행한다.

// 변경 사항을 먼저 commit $ git add . $ git commit -m ‘Commit before eject’ // CRA 의 웹팩 관련 설정이 기본적으로 모두 숨겨져있어 이를 밖으로 추출해주는 과정. $ yarn eject

git 작업을 해주지 않으면,

이런 에러가 뜨니 commit 을 해주고 실행해주면 된다.

그리고 주의사항. yarn eject 를 한번 실행하게 되면 다시 되돌릴 수 없다는 경고문구가 나오는만큼, 한번 추출하면 이전 상태로 되돌릴 수 없다! 이 점에 유의.

그리고 CRA 에서의 장점인 One build Dependency 의 장점을 잃게 된다. 작업 도중 하나의 패키지가 필요해서 설치한다거나,삭제할 때는 항상 다른 패키지들과의 의존성을 생각해야하고, Webpack 과 Babel 설정까지 익숙하지 않다면…. ㅎㅁㅎ.. 많은 설정을 변경해야하는 경우에만 씁시다

다른 커스터마이징을 제공하는 라이브러리가 있지만 eject 보다 자유롭지는 않고, eject의 단점인 안정성을 라이브러리가 완전히 보장해주지는 않는다.

서버 사이드 렌더링을 하기 위한 과감한 선택…..yarn eject 실행.

실행을 하게 되면 이렇게 숨겨진 파일들이 톡하고 튀어나온다.

이제 커스텀 ㄱ!

엔트리

웹팩에서 프로젝트를 불러올 때 가장 먼저 불러오는 파일. ( 현재 플젝에서는 index.js 파일을 엔트리로 사용 )

해당 파일부터 시작하여, 내부에 필요한 다른 컴포넌트와 모듈을 불러온다.

서버 사이드 렌더링을 위해서는 서버를 위한 엔트리 파일을 따로 생성해야 한다.

src/index.server.js ( 새로 생성 )

가장 기본적인 코드 형태.

이제 이 엔트리 파일을 웹팩으로 불러와서 빌드하려면 서버 전용 환경 설정을 만들어 주어야함.

src/paths.js

paths 파일 맨 밑에 서버 사이드 렌더링 엔트리 경로와, 웹팩 처리 후 저장 경로 이 두 가지를 추가해준다. ( 주석으로 단 부분 추가만 해주면 됨. )

config/webpack.config.server.js ( 새로 생성 )

// paths module import const paths = require(‘./paths’); module.exports = { mode:’production’, // 프로덕션 모드로 설정하여 최적화 옵션들을 활성화 entry:paths.ssrIndexJs, // 엔트리 파일 경로 target:’node’, // node 환경에서 실행될 것이라는 점을 명시 output:{ path:paths.ssrBuild, // 빌드 경로 filename:’server.js’, // 파일 이름 chunkFilename:’js/[name].chunk.js’, // chunk file 이름 publicPath:paths.publicUrlOrPath // 정적 파일이 제공될 경로 } }

그 다음 웹팩 환경설정 부분을 파일을 새로 생성해 적어주었다.

빌드시 어떤 파일에서 시작해 파일을 불러오는지, 또 어디에 결과물을 저장할건지를 정해준 것.

그 다음으로 설정해야할 것은 3가지!

첫번째로는 로더를 설정해야 하는데, 웹팩에서의 로더는 파일을 불러올 때 확장자에 맞게 필요한 처리를 해준다.

보통 웹팩을 사용하면 이전 자바스크립트 코드를 변환해주고, JSX 같은 문법을 브라우저에서 사용하기 위해 babel 을 사용해준다.

또, CSS 는 모든 CSS 코드를 결합, 이미지 파일은 파일을 다른 경로에 따로 저장하여 그 파일에 대한 경로를 자바스크립트에서 참조할 수 있게 설정해준다.

서버 사이드 렌더링을 할 때에는 CSS 혹은 이미지 파일이 엄청 중요도를 가지는 것은 아니지만 완전히 무시할 수는 없기에 ( 자바스크립트 내부에서 파일에 대한 경로 필요, CSS Module 에서 로컬 className 을 참고 할 때 등등) 해당 파일을 로더에서 별도로 설정하여 처리할 것은 처리하지만, 따로 결과물에 포함되지 않도록 구현할 수 있다.

또한, 브라우저에서 사용할 때는 결과물 파일에 리액트 라이브러리와 나의 애플리케이션에 관한 코드가 공존하는데, 서버에서는 라이브러리가 굳이 결과물 파일안에 들어있지 않아도 된다. node_modules 를 통해 바로 불러와서 사용할 수 있기 때문 ㅇㅇ 그래서 서버를 위해 번들링할 때는 webpack-node-extension 이라는 라이브러리를 사용해서 node_modules에서 불러오는 것을 제외하고 번들링하게 설정할 수 있음.

이 라이브러리는 설치가 필요하니 yarn 명령어로 설치를 해주면 된다.

$ yarn add webpack-node-externals

마지막으로 환경변수를 주입해주어야 한다. 이를 주입해주면, 프로젝트 내에서 process.env_NODE_ENV 값을 참조하여 현재 개발 환경인지 아닌지를 알 수 있다.

먼 말인지 나도 다는 이해 안됨 ㅎ 책에서 나온 설정 역시

출처 – https://webpack.js.org/configuration/

요기를 참고했다고 하니 ..! 꼭 하나하나 다 알 필요는 없을 것 같다 ㅎ

config/webpack.config.server.js

// webpack-node-externals 라이브러리 import const nodeExternals = require(“webpack-node-externals”); const paths = require(“./paths”); const getCSSModuleLocalIdent = require(“react-dev-utils/getCSSModuleLocalIdent”); const webpack = require(“webpack”); const getClientEnvironment = require(“./env”); const cssRegex = /\.css$/; const cssModuleRegex = /\.module\.css$/; const sassRegex = /\.(scss|sass)$/; const sassModuleRegex = /\.module\.(scss|sass)$/; // 환경변수 const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1)); module.exports = { mode: “production”, // 프로덕션 모드로 설정하여 최적화 옵션들을 활성화 entry: paths.ssrIndexJs, // 엔트리 파일 경로 target: “node”, // node 환경에서 실행될 것이라는 점을 명시 // 결과물이 어떻게 나올지를 명시 output: { path: paths.ssrBuild, // 빌드 경로 filename: “server.js”, // 파일 이름 chunkFilename: “js/[name].chunk.js”, // chunk file 이름 publicPath: paths.publicUrlOrPath, // 파일들이 위치할 서버 상의 경로 . }, //loader module: { rules: [ { oneOf: [ // 자바스크립트를 위한 처리 // 기존 webpack.config.js 를 참고하여 작성 { test: /\.(js|mjs|jsx|ts|tsx)$/, include: paths.appSrc, loader: require.resolve(“babel-loader”), options: { customize: require.resolve( “babel-preset-react-app/webpack-overrides” ), presets: [ [ require.resolve(“babel-preset-react-app”), { runtime: “automatic”, }, ], ], // import 에 대한 플러그인 plugins: [ [ require.resolve(“babel-plugin-named-asset-import”), { loaderMap: { svg: { ReactComponent: “@svgr/webpack?-svgo,+titleProp,+ref![path]”, }, }, }, ], ], cacheDirectory: true, cacheCompression: false, compact: false, }, }, // CSS 를 위한 처리 { test: cssRegex, exclude: cssModuleRegex, loader: require.resolve(“css-loader”), options: { importLoaders: 1, modules: { // exportOnlyLocals: true 옵션을 설정해야 실제 css 파일을 생성하지 않음. exportOnlyLocals: true, }, }, }, // CSS Module 을 위한 처리 { test: cssModuleRegex, loader: require.resolve(“css-loader”), options: { importLoaders: 1, modules: { exportOnlyLocals: true, getLocalIdent: getCSSModuleLocalIdent, }, }, }, // Sass 를 위한 처리 { test: sassRegex, exclude: sassModuleRegex, use: [ { loader: require.resolve(“css-loader”), options: { importLoaders: 3, modules: { exportOnlyLocals: true, }, }, }, require.resolve(“sass-loader”), ], }, // Sass + CSS Module 을 위한 처리 { test: sassRegex, exclude: sassModuleRegex, use: [ { loader: require.resolve(“css-loader”), options: { importLoaders: 3, modules: { exportOnlyLocals: true, getLocalIdent: getCSSModuleLocalIdent, }, }, }, require.resolve(“sass-loader”), ], }, // url-loader 를 위한 설정 { test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], loader: require.resolve(“url-loader”), options: { emitFile: false, // 파일을 따로 저장하지 않는 옵션 limit: 10000, // 원래는 9.76KB가 넘어가면 파일로 저장하는데 // emitFile 값이 false 일땐 경로만 준비하고 파일은 저장하지 않음. name: “static/media/[name].[hash:8].[ext]”, }, }, // 위에서 설정된 확장자를 제외한 파일들은 // file-loader 를 사용. { loader: require.resolve(“file-loader”), exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/], options: { emitFile: false, // 파일을 따로 저장하지 않는 옵션 name: “static/media/[name].[hash:8].[ext]”, }, }, ], }, ], }, // react, react-dom/server 같은 라이브러리를 import 구문으로 불러오게 되면, node_modules 에서 찾아서 불러오기 위한 설정. 라이브러리를 불러오면 빌드할 때 결과물 파일 안에 해당 라이브러리 관련 코드가 함께 번들링. resolve: { modules: [“node_modules”], }, // 서버를 위해 번들링할 때는 node-modules 에서 불러오는 것을 제외하고 번들링하는 것이 좋음. 이를 위해 webpack-node-extenrnals 라이브러리 사용 externals: [nodeExternals()], plugins: [ new webpack.DefinePlugin(env.stringified), // 환경변수를 주입. ], };

매우 매우 매우 길군…..ㅎ….. 책에는 정말 간단하게 주석으로 정리해놔서 책부분 + 웹팩 부분 구글링 해서 쪼오오금의 주석을 더 추가했다. 나머지는 주절주절로 위에 줄글로 정리해보아쑴 ㅎ

이제 환경설정은 끝!

이제 이 환경설정을 사용해서 웹팩으로 프로젝트를 빌드하는 스크립트를 작성.

src/scripts/build.js 이 경로에 있는 파일은 클라이언트에서 사용할 빌드 파일이다. 우리는 얘랑 비슷하게 서버에서 사용할 빌드 파일을 만드는 build.server.js 스크립트를 작성하면 됨.

src/scripts/build.server.js ( 새로 생성 )

process.env.BABEL_ENV = ‘production’; process.env.NODE_ENV = ‘production’; process.on(‘unhandledRejection’, (err) => { throw err; }); require(‘../config/env’); const fs = require(‘fs-extra’); const webpack = require(‘webpack’); const config = require(‘../config/webpack.config.server’); const paths = require(‘../config/paths’); function build() { console.log(‘Creating server build…’); fs.emptyDirSync(paths.ssrBuild); let compiler = webpack(config); return new Promise((resolve, reject) => { compiler.run((err, stats) => { if (err) { console.log(err); return; } console.log(stats.toString()); }); }); } build();

요롷게 빌드 파일 작성해서 명령어를 통해 빌드가 잘되는지를 확인.

$ node scripts/build.server.js

요롷게 잘 빌드가 된다면, 이제 실행을 시험삼아 해보자.

node dist/server.js

갸악 세상에 index.server.js 에서 썼던대로 문자열 형태로 렌더링되서 출력해주는군

근데 실행할 때마다 파일 경로 치기 넘나 귀차느니까 설정을 해줘서 간단하게 명령어를 칠 수 있도록 하자.

package.json

scripts 부분에 이렇게 적어쥼~~~

굿 이제 간단하게 빌드랑 서버 명렁어도 잘 설정됐음.

ㅎ………..이제 구현 준비 완료가 된 것뿐 이제 시작임…갈 길이 구만리에요…………………ㅠ

서버 사이드 렌더링을 처리할 서버를 Node.js 의 프레임워크인 Express 를 사용해서 만들겠음.

다른 Koa, Hapi 등등 connect 라이브러리를 사용하면 구현할 수 있지만서도 Express 가 사용률이 가장 높고, 추후 정적 파일들을 호스팅 할 때에도 쉽게 구현할 수 있기 때문!

$ yarn add express

src/index.server.js

import React from “react”; import ReactDOMServer from “react-dom/server”; // express import import express from “express”; // StaticRouter import import { StaticRouter } from “react-router-dom”; // App import import App from “./App”; const app = express(); // 서버 사이드 렌더링을 처리할 핸들러 함수. // 이 함수는 page not found 404가 떠야 하는 상황에서 404 에러를 띄우지 않고 서버 사이드 렌더링을 해줌. const serverRender = (req, res, next) => { const context = {}; const jsx = ( ); const root = ReactDOMServer.renderToString(jsx); // 렌더링 후 res.send(root); // 클라이언트에게 결과물 응답 }; app.use(serverRender); app.listen(5000, () => { console.log(“Runninng on http://localhost:5000”); });

여기서 사용된 StaticRouter 컴포넌트는 주로 서버 사이드 렌더링 용도로 사용되는 라우터인데, props 로 넣어주게 되는 location 값에 따라 라우팅해준다. req 객체는 요청에 대한 정보를 지니는데, 이의 url 을 props 로 넣어주었음.

또 context props 는 이 값을 사용해서 나중에 렌더링한 컴포넌트에 따라 HTTP 상태코드를 설정 해줄 수 있도록 해준 것이다.

또 맨 마지막 함수는 5000번의 포트로 서버를 가동하기 위해 사용되었다.

리액트는 3000번 포트, 장고는 8000번 포트를 사용했던 것처럼 따로 우리 서버는 5000번 포트를 가동하도록 설정해준 것! ( 8000번… 갑자기 그리워짐…. 장고 돌아와……)

어질어질하다………

당장은 JS,CSS 파일을 웹 페이지에 불러오는 것을 생략하고, 리액트 서버 사이드 렌더링을 통해 만들어진 결과만 보여주도록 처리했다.

// build $ yarn build:server // 실행 $ yarn start:server

해주고 localhost:5000 경로로 접속.

실행 화면

흑..흑 CSS 불러오지는 않고, 브라우저에서 자바스크립트도 실행되지 않지만 이 모든 것들은 서버 사이드에서 렌더링된 결과물이다.

근데 자바스크립트가 클라이언트 딴에서 실행된건지 아니면 서버에서 렌더링된 건지 분간이 어렵다면,

개발자 도구의 Network 탭에서 새로고침을 하고 맨 위의 파일을 클릭하고 해당 Response 를 보면 이렇게 컴포넌트의 렌더링 결과가 문자열로 전달된 것을 볼 수 있다.

참고로 이 {} 버튼 누르면 들여쓰기 때문에 더 편하게 봐짐.

이제 CSS, JS 정적 파일들에 접근할 수 있도록 Express 에 내장된 미들웨어 static 을 사용!

import React from “react”; import ReactDOMServer from “react-dom/server”; // express import import express from “express”; // StaticRouter import import { StaticRouter } from “react-router-dom”; // App import import App from “./App”; //path import import path from ‘path’; const app = express(); // 서버 사이드 렌더링을 처리할 핸들러 함수. // 이 함수는 page not found 404가 떠야 하는 상황에서 404 에러를 띄우지 않고 서버 사이드 렌더링을 해줌. const serverRender = (req, res, next) => { const context = {}; const jsx = ( ); const root = ReactDOMServer.renderToString(jsx); // 렌더링 후 res.send(root); // 클라이언트에게 결과물 응답 }; const serve = express.static(path.resolve(‘./build’,{ index:false // “/” 경로에서 index.html 을 보여주지 않도록 설정. })) //// 반드시 serverRender 위에 위치할것. app.use(serve); ///////////////// app.use(serverRender); app.listen(5000, () => { console.log(“Runninng on http://localhost:5000”); });

여기서 serve 부분만 참고. 캡쳐본을 올리니까 더 헷갈려하는 사람들이 쩜 있길래 ㅎㅅㅎ path import 도 잊지말기.

그 다음에는 JS , CSS 파일을 불러오도록 html 에 코드를 삽입해 주어야 하는데, 불러와야 할 파일 이름은 매번 빌드를 할 때마다 바뀌기 때문에 빌드하고 나서 만들어지는 파일을 참고하여 불러오도록 해야 하는데, 그 파일은 build/asset-mainfest.json 임!

yarn build ( yarn build:server 아님 )후에 build 디렉토리가 생성되면, 그 안의 asset-mainfest.json 파일을 살펴보자.

build/asset-mainfest.json

여기서 몇개의 부분을 html 내부에 적어주어야 한다.

src/index.server.js

import React from “react”; import ReactDOMServer from “react-dom/server”; // express import import express from “express”; // StaticRouter import import { StaticRouter } from “react-router-dom”; // App import import App from “./App”; //path import import path from ‘path’; // fs import import fs from ‘fs’; // asset-manifest.json에서 파일 경로들을 조회 const manifest = JSON.parse( fs.readFileSync(path.resolve(‘./build/asset-manifest.json’), ‘utf8’) ); const chunks = Object.keys(manifest.files) .filter(key => /chunk\.js$/.exec(key)) // chunk.js로 끝나는 키를 찾아서 .map(key => ``) // 스크립트 태그로 변환하고 .join(”); // 합침 function createPage(root, tags) { return ` React App

${root}

${chunks} `; } const app = express(); // 서버 사이드 렌더링을 처리할 핸들러 함수. // 이 함수는 page not found 404가 떠야 하는 상황에서 404 에러를 띄우지 않고 서버 사이드 렌더링을 해줌. const serverRender = (req, res, next) => { const context = {}; const jsx = ( ); const root = ReactDOMServer.renderToString(jsx); // 렌더링 후 res.send(createPage(root)); // 클라이언트에게 결과물 응답 }; const serve = express.static(path.resolve(‘./build’),{ index:false // “/” 경로에서 index.html 을 보여주지 않도록 설정. }); //// 반드시 serverRender 위에 위치할것. app.use(serve); ///////////////// app.use(serverRender); app.listen(5000, () => { console.log(“Runninng on http://localhost:5000”); });

CSS,JS 까지 모두 불러온 기본 서버 사이드 렌더링 최종본 ..! html 부분은 복붙해서 그냥 스윽 보는 것을 추천…ㅎ..!! 불러오는 거뿐이니까 말이당. import 부분은 fs 만 추가해서 asset-manifest.json 에서 파일 경로를 조회할 수 있도록 했다.

이제 빌드, 다시 시작.

$ yarn build:server $ yarn start:server

실행 화면

네트워크 탭에서 보면 이렇게 스크립트태그도 잘 들어가 있고, css 파일도 적용이 잘되어 나타남 짝짜ㅏㅏ짜짝

그리고 주의할 점은 처음 렌더링은 서버를 통해 하게 되지만, red,blud 링크를 이동할 때의 렌더링은 브라우저에서 해야함. 다른 페이지로 이동할 때 네트워크의 추가적인 요청이 개발자 도구에 뜨면 안됨!

와! 기본적인 서버 사이드 렌더링 끝ㅌㅌㅌㅌ!!!!! 아직 갈길이 멀지만 기초만 잘 다져도 될 것….후…. 화이팅화이팅…흑…흑 쨋든 리액트에서의 서버 사이드 렌더링 확인-!

왜 React와 서버 사이드 렌더링인가?

read

홈쇼핑처럼 4번째 상품인 튀김을 기름에 튀기면서 React를 적용하느라 고생했던 순간이 떠올라 React와 서버 사이드 렌더링 적용과정을 정리해봅니다. 여기서는 어떻게보다는 왜에 대해 설명합니다.

현재 홈쇼핑처럼의 커머스 백엔드는 PHP로 만든 Magento를 사용하고 있고 프론트엔드는 일부 화면에서 React를 사용하고 있습니다. 웹서버에 v8js extention을 설치하여 서버 사이드 렌더링을 지원하고 있습니다. 그리고 홈쇼핑처럼에서 사용중인 채팅은 Rails로 만든 별도의 서비스로 분리하여 운영하고 있고 관리자 페이지 프론트엔드는 역시 React에 therubyracer라는 V8 javascript interpreter를 이용하여 서버 사이드 렌더링을 지원하고 있습니다. 이전에 작업했던 .NET 프로젝트에도 프론트엔드는 React에 ReactJS.NET를 이용하여 서버 사이드 렌더링을 지원하였습니다.

백엔드는 프로젝트마다 달라도 프론트엔드는 React에 서버사이드 렌더링을 지원하는 방식으로 통일하고 있습니다. React가 뭐길래, 서버사이드 렌더링이 뭐가 좋길래 이런 결정을 한걸까요?

먼저, 자바스크립트를 이용한 웹 개발의 발전방향을 살펴봅니다.

지금은 Javascript를 이용하여 동적인 웹페이지를 자연스럽게 만들고 있지만 10년전 ajax가 등장하기 전에는 서버에서 전체 HTML을 만드는 방식이 일반적이였습니다.

connect_error) { die(“Connection failed: ” . $conn->connect_error); } $sql = “SELECT id, firstname, lastname FROM MyGuests”; $result = $conn->query($sql); if ($result->num_rows > 0) { // output data of each row while($row = $result->fetch_assoc()) { echo “id: ” . $row[“id”]. ” – Name: ” . $row[“firstname”]. ” ” . $row[“lastname”]. “
“; } } else { echo “0 results”; } $conn->close(); ?>

react-router :: 3장. 서버사이드 렌더링

이 튜토리얼은 3개의 포스트로 나뉘어진 이어지는 강좌입니다. 목차를 확인하시려면 여기를 참고하세요.

3장. 서버사이드 렌더링

이번 장에서는 리액트 어플리케이션을 서버사이드 렌더링 하는 방법을 알아보겠습니다. 여러분이 리액트를 통해 어플리케이션을 개발하게 될 때, 서버사이드 렌더링을 구현 할 수도, 하지 않을 수도 있습니다. 필수작업은 아니기 때문이지요. 서버사이드 렌더링을 구현하기 전에, 먼저 서버사이드 렌더링이 실제로 여러분의 프로젝트에 정말 필요한지 고려해볼 필요가 있습니다.

확실히, 서버사이드렌더링은 멋집니다. 어떤 상황에서는 필수일수도 있지요. 하지만 단점 또한 존재합니다.

서버사이드 렌더링을 통하여 얻을 수 있는 이점

검색엔진 최적화

서버사이드 렌더링을 통하여 얻을 수 있는 가장 큰 이점은 검색엔진 최적화 입니다. 리액트, 혹은 다른 자바스크립트 라이브러리/프레임워크로 만들어져 뷰 렌더링이 자바스크립트 위주로 돌아가는 프로젝트는, 자바스크립트 엔진이 돌아가지 않으면 원하는 정보를 표시해주지 않습니다.

한번, 리액트로 만든 프로젝트를 브라우저로 열어서 페이지 우클릭을하여 소스보기를 해보세요.

내용이 비어있습니다.

따라서, 이렇게 클라이언트 렌더링만 될 경우엔 검색엔진 크롤러가 여러분의 어플리케이션이 지닌 데이터들을 제대로 수집하지 못합니다. 하지만 그렇다고 해서 너무 걱정하지는 마세요. 검색엔진중 가장 중요한 구글 검색엔진은 크롤러에 자바스크립트 엔진이 내장되어 있어서 우리가 별도로 작업을 하지 않아도, 우리가 준비한 데이터를 제대로 렌더링해줍니다.

하지만, 네이버, 다음 등의 검색엔진이 사이트를 제대로 크롤링 하게 지원해야한다면, 서버사이드 렌더링을 따로 구현하셔야 합니다.

성능 개선

서버사이드 렌더링을 하게되면, 첫 렌더링된 html 을 클라이언트에게 전달을 해주기때문에 초기로딩속도를 많이 줄여줄 수 있습니다. 자바스크립트 파일을 불러오고, 렌더링 작업이 완료되기 전에도, 유저가 사이트의 컨텐츠를 이용 할 수 있게 됩니다.

서버사이드 렌더링의 단점

프로젝트의 복잡도

서버사이드 렌더링을 구현하게 된다면, 프로젝트의 구조가 많이 복잡해지게 됩니다. 단순히 렌더링만 하는것은 그렇게 큰 문제는 아니지만, 리액트 라우터, 리덕스 등의 라이브러리와 함께 연동해서 사용하면서, 서버에서 데이터를 가져와서 렌더링을 해줘야하는 경우엔 조금 어려워질수도 있습니다.

하지만… 걱정하지마세요! 이번 3장에서 나오는 가이드를 따라하면 시간을 많이 낭비하지 않고 라우터와 데이터 가져오기까지 큰 수고 없이 구현 할 수 있게 될 것입니다.

성능의 악화 가능성

장점이 성능 개선이라고 했는데, 성능이 악화될수도 있다니 이게 무슨소리인가 싶지요? 클라이언트에서 초기렌더링을 서버측에서 대신 해주니까, 클라이언트의 부담을 없애주기는 하지만, 그 부담은 서버가 가져가게 됩니다. 서버사이드 렌더링을 하게 될 때는, ReactDOMServer.renderToString 함수를 사용하게 되는데요, 이 함수는 동기적으로 작동합니다. 그래서, 렌더링하는동안은 이벤트루프가 막히게 됩니다. 그 뜻은, 렌더링이 되는 동안은, 다른 작업을 못한다는 뜻입니다. 만약에 여러분의 프로젝트를 렌더링하는데 250ms 가 걸린다면 1초에 4개의 요청밖에 처리하지 못한다는 의미입니다. 치명적이죠.

비동기식 렌더링

하지만.. 걱정하지마세요! 써드파티 라이브러리를 통하여 비동기식으로 작동하게끔 코드를 작성 할 수 있습니다. 이를 가능케 하는 라이브러리는 여러가지가 있는데요:

rapscallion: renderToString 을 비동기로 해주며, Promise 기반으로 작동하고, 컴포넌트 단위로 캐싱을 할 수 있습니다

hypernova: airbnb 에서 만든 도구로서, 렌더링을 위한 서버를 따로 열어서 cluster 를 사용하여 여러 프로세스를 생성하여 렌더링을하고, 운영서버에서 렌더링서버로 결과물을 요청을 하는 방식으로 작동합니다.

를 사용하여 여러 프로세스를 생성하여 렌더링을하고, 운영서버에서 렌더링서버로 결과물을 요청을 하는 방식으로 작동합니다. react-router-server: react-router v4 를 위해 만들어진 서버사이드 렌더링 라이브러리로서, Promise 비동기식 렌더링을 지원해주고, 깔끔한 방식으로 데이터를 불러올 수도 있습니다. 우리는 이 라이브러리를 통하여 서버사이드 렌더링을 구현 할 것입니다.

최적화 성능의 악화를 막기위한 또 다른 방법은, 서버사이드 렌더링을 촤적화 시키는 것 입니다. 서버의 성능이 그렇게 좋지 않는데 네이버/다음 등의 검색엔진을 지원해야한다면, 요청이 들어왔을 때, 검색엔진 크롤러 봇인 경우에만 서버사이드 렌더링을 하는 방식으로 구현 할 수 도 있습니다. 추가적으로, 개인화된 데이터는 서버사이드 렌더링하는것을 피하고, (예: 로그인된 사용자의 뷰) 모든 유저에게 같은 형식으로 보여지는 뷰들을 캐싱하는 것도 좋은 방안입니다.

또 다른 대안

메타 태그만 넣어주기

리액트 어플리케이션을 렌더링하지는 않고, 서버쪽에서 라우트에 따라 필요한 메타태그만 넣어주는 것 입니다. 그러면, 크롤러에선 해당 페이지에 대한 기본 정보는 얻어 갈 수 있게 됩니다. SNS 공유를 할 때 내용이 잘 전달되게 하기 위한것이라면 매우 적합한 방식입니다.

Prerender

만약에 검색엔진 최적화 때문에 서버사이드 렌더링을 구현해야하는데, 프로젝트의 구조가 복잡해지는게 싫고, 또 성능이 받쳐주지 않는다면, 또 다른 대안이 있습니다. 바로, prerender 라는 서비스를 이용하는 것 입니다.

Prerender 는 리액트 코드를 문자열 형태로 변환을 하는게 아니라, 아예 자바스크립트 렌더링 엔진을 가지고 있어서, 자바스크립트 코드를 실행시켜 뷰를 렌더링한 결과값을 반환합니다. 렌더링 속도는 그렇게 빠르지 않기 때문에, 이 서비스는 오직 검색엔진 최적화를 위해서만 사용됩니다. 크롤러 봇일 경우에만 대신 렌더링을 해줘서 반환을 해주는것이죠.

이 서비스는 유료 서비스인데, 페이지의 수, 그리고 캐싱 주기에 따라 가격이 달라집니다. 250개의 페이지까지는 무료인데요, 그 이상부턴 유료로 전환됩니다. 20000 개 정도의 페이지를 7일 주기로 캐싱하는것은 매달 $15 로 이용 할 수 있으며, 페이지 수가 더 많아지면 가격이 더 비싸집니다.

더욱 멋진것은, Prerender 는 오픈소스로 공개가 되어서, 여러분의 서버에서 직접 무료로 돌릴수도 있다는 점 입니다. 매뉴얼에서는 검색엔진 최적화는 특권이 아닌 권리이기 때문에 오픈소스로 공개했다고 하는데요. 참 멋진 서비스이죠?

3-1. Koa 사용하기

소개

서버사이드 렌더링을 시작하기에 앞서, 우리가 웹서버를 열기 위해서 사용 할 웹프레임워크 Koa 를 설치하고 서버를 열어보도록 하겠습니다. Koa 는 Node.js 의 가장 인기있는 웹프레임워크인 Express 를 창시했던 개발팀이 만든 더 가벼운 웹프레임워크입니다. (Express 는 StrongLoop 으로 소유권이 이전되었습니다) 이 프레임워크는 최신 Node.js 버전에 맞춰서 설계되었으며, async await 문법을 사용 할 수있고, 에러 관리하기가 쉬워서 비동기적 작업을 하다가 코드 구조가 매우 복잡해져 버리는 콜백지옥이 생겨나지 않습니다.

설치 및 서버 작성

자, 그럼 설치를 해줍시다.

$ yarn add koa

그리고, 서버 파일을 작성해보세요.

server/index.js

const Koa = require ( ‘koa’ ); const app = new Koa(); app.use(ctx => { ctx.body = ‘Hello World’ ; }); app.listen( 3001 );

서버를 작성하셨으면 다음 명령어를 통하여 서버를 실행하세요.

$ node server

브라우저에서 http://localhost:3001/ 에 접속해보세요.

Hello World 가 잘 나타나나요?

정적 파일 제공하기

서버사이드 렌더링을 구현하기에 앞서, 먼저 js 파일 및 css 파일들을 서버에서 제공하는 코드를 작성하겠습니다. 정적 파일들을 제공할 때에는, koa-static 미들웨어를 사용하시면 됩니다.

이를 설치해주세요.

$ yarn add koa-static

그 다음엔, 서버에서 koa-static 을 불러온 다음에 미들웨어를 적용하세요. 웹 요청이 들어왔을 때, build 디렉토리에 알맞는 파일이 있으면 해당 파일을 반환하고, 그렇지 않으면 Hello World 가 반환됩니다.

server/index.js

const Koa = require ( ‘koa’ ); const serve = require ( ‘koa-static’ ); const path = require ( ‘path’ ); const app = new Koa(); app.use(serve(path.resolve(__dirname, ‘../build/’ ))); app.use(ctx => { ctx.body = ‘Hello World’ ; }); app.listen( 3001 );

이제 리액트 앱을 한번 빌드하고, 서버를 재시작하세요.

$ yarn build $ node server

그리고 다시 브라우저에서 http://localhost:3001/ 에 접속해보세요.

3001 포트로도 뷰가 잘 렌더링 될 것입니다. 이제 about 페이지에 들어가서 새로고침을 해보세요. 잘 되죠? 하지만 이건 create-react-app 에서 적용된 서비스 워커 때문에 되는 것 입니다. 한번 개발자 도구를 열은 다음 브라우저 왼쪽 상단의 새로고침 버튼을 우클릭 하여 캐시 비우기 및 강력 새로고침을 해보세요.

Hello World 가 뜰 것입니다.

클라이언트 라우팅이 제대로 작동하게 하려면, 서버측에서 준비되지 않은 요청이 들어 왔을 시, 리액트 어플리케이션이 띄워져있는 index.html 의 내용을 보여주어야 합니다.

Node.js 내장 모듈인 fs 를 통하여 index.html 을 불러온 다음에, 내용을 반환하도록 설정하겠습니다.

server/index.js

const Koa = require ( ‘koa’ ); const serve = require ( ‘koa-static’ ); const path = require ( ‘path’ ); const fs = require ( ‘fs’ ); const app = new Koa(); const indexHtml = fs.readFileSync(path.resolve(__dirname, ‘../build/index.html’ ), { encoding: ‘utf8’ }); app.use(serve(path.resolve(__dirname, ‘../build/’ ))); app.use(ctx => { ctx.body = indexHtml; }); app.listen( 3001 );

이제 캐시를 비워도 제대로 작동 할 것입니다.

지금까지 한 작업은 서버에서 리액트 어플리케이션을 단순히 전송만 한 것이며, 서버사이드 렌더링은 아직 구현하지 않은 상태 입니다.

리액트를 렌더링 하려면, 서버측에서 리액트 컴포넌트들을 불어와야 하는데요, Node.js 에서는 기본적으로는 jsx 를 불러올 수 없습니다. 추가적으로, import 를 통하여 파일을 불러 올 수도 없죠. 따라서, babel 을 사용해야하는데요. 이 방법은 크게 4가지로 나뉘어집니다.

babel-node 를 통해 런타임에서 babel 사용하기: 이 방법은 프로덕션에선 좋지 않는 방식입니다. 서버에서 코드를 변환하기 위하여 불필요한 자원이 사용되기 때문이죠. babel-register 를 서버에서 불러와서 사용하기: 이 방법 또한 런타임에서 코드를 변환하여 사용하는 방식이며 1번과 같은 이유로 추천되지 않습니다. babel 을 통하여 서버를 빌드한 다음에 사용하기: 서버를 실행하기전에 사전에 코드를 트랜스파일 하여 서버를 실행하는 것 입니다. 서버를 수정 할 때마다 다시 트랜스파일링을 해야하므로, 개발중엔 코드를 수정할때마다 중간 중간 딜레이가 발생하는 단점이 있습니다. webpack 을 통하여 리액트 관련 코드만 미리 빌드해서 사용하기: 이 방법이 성능상에서, 그리고 개발 흐름상에서 가장 괜찮은 방법입니다. 결국엔 babel 은 리액트쪽 코드에서만 사용되는거니까, 리액트 관련 코드를 미리 webpack 으로 번들링 한 것을 그냥 require 로 불러와서 사용하는 방식입니다.

다음 섹션에서는, 방금 소개된 4번째 방법을 통하여 개발을 진행하겠습니다.

3-2. 서버사이드 렌더링 준비하기

서버용 엔트리 생성

자, 이제 본격적으로 서버사이드 렌더링을 준비해도록 하겠습니다. 클라이언트에서의 엔트리 파일은 src/index.js 였지요? 이 파일에서는 브라우저 상에서 id 가 root 인 DOM 을 찾아서 렌더링을 해주었습니다.

서버사이드 렌더링을 할 때에는, 서버를 위한 엔트리 파일을 따로 만들어주어야합니다.

다음 파일을 생성하세요:

src/server/render.js

import React from ‘react’ ; import ReactDOMServer from ‘react-dom/server’ ; import { StaticRouter } from ‘react-router’ ; import App from ‘shared/App’ ; const render = (location) => ReactDOMServer.renderToString( < StaticRouter location = {location} > < App /> ); export default render;

ReactDOMServer.renderToString 은 JSX 를 HTML 문자열로 변환을 해줍니다. 위 코드의 render 함수는 서버에서 전달받은 요청의 경로인 location 을 전달받아서 renderToString 을 실행시켜줍니다.

그리고 default 로 내보내기를 했지요.

이제 이것을 webpack 으로 번들링 해주면 됩니다.

서버용 웹팩 환경설정 생성

먼저 config 디렉토리의 paths.js 파일을 열어서 하단의 배열에 다음 두 값을 추가하세요.

config/paths.js – module.exports

module .exports = { serverRenderJs: resolveApp( ‘src/server/render.js’ ), server: resolveApp( ‘server/render’ ) };

서버 엔트리의 위치와, 번들링 후 저장 할 경로를 지정해주었습니다.

서버용 웹팩설정은 webpack.config.prod.js 를 기반으로 만듭니다. 차이점은, 불필요한 웹팩 플러그인을 모두 없앴고, 로더 또한, js, jsx, json 확장자를 제외한 파일들은 모두 무시하도록 설정됩니다.

불러오는 자바스크립트 이외의 파일들은 무시하려면, ignore-loader 를 설치해야합니다.

$ yarn add ignore-loader –dev

설치 후에는, 다음 웹팩 설정 파일을 생성하세요. 기존 webpack.config.prod.js 에서 불필요한 코드를 지우고 조금 수정을 해준 환경설정입니다. 주요 부분은 주석이 되어있으니 참고하세요.

config/webpack.config.server.js

; const path = require ( ‘path’ ); const webpack = require ( ‘webpack’ ); const CaseSensitivePathsPlugin = require ( ‘case-sensitive-paths-webpack-plugin’ ); const WatchMissingNodeModulesPlugin = require ( ‘react-dev-utils/WatchMissingNodeModulesPlugin’ ); const getClientEnvironment = require ( ‘./env’ ); const paths = require ( ‘./paths’ ); const publicUrl = ” ; const env = getClientEnvironment(publicUrl); module .exports = { entry: paths.serverRenderJs, target: ‘node’ , output: { path: paths.server, filename: ‘render.js’ , libraryTarget: ‘commonjs2’ }, resolve: { modules: [ ‘node_modules’ , paths.appNodeModules].concat( process.env.NODE_PATH.split(path.delimiter).filter( Boolean ) ), extensions: [ ‘.js’ , ‘.json’ , ‘.jsx’ ], }, module : { strictExportPresence: true , rules: [ { exclude: [ /\.(js|jsx)$/ , /\.json$/ ], loader: ‘ignore’ , }, { test: /\.(js|jsx)$/ , include: paths.appSrc, loader: require .resolve( ‘babel-loader’ ), options: { cacheDirectory: true , }, } ], }, plugins: [ new webpack.DefinePlugin(env.stringified), new CaseSensitivePathsPlugin(), new WatchMissingNodeModulesPlugin(paths.appNodeModules), ] };

서버용 빌드 스크립트 생성

이제 빌드를 위한 스크립트도 만들어주겠습니다. 이 파일 또한 scripts 디렉토리의 build.js 를 기반으로 조금 수정하고, 불필요한 코드를 삭제하여 만들어진 코드입니다.

scripts/build.server.js

; process.env.BABEL_ENV = ‘production’ ; process.env.NODE_ENV = ‘production’ ; process.env.APP_ENV = ‘server’ ; process.on( ‘unhandledRejection’ , err => { throw err; }); require ( ‘../config/env’ ); const webpack = require ( ‘webpack’ ); const config = require ( ‘../config/webpack.config.server’ ); const paths = require ( ‘../config/paths’ ); const checkRequiredFiles = require ( ‘react-dev-utils/checkRequiredFiles’ ); const formatWebpackMessages = require ( ‘react-dev-utils/formatWebpackMessages’ ); if (!checkRequiredFiles([paths.serverRenderJs])) { process.exit( 1 ); } function build () { console .log( ‘Creating an server production build…’ ); let compiler = webpack(config); return new Promise ((resolve, reject) => { compiler.run((err, stats) => { if (err) { return reject(err); } const messages = formatWebpackMessages(stats.toJson({}, true )); if (messages.errors.length) { return reject( new Error (messages.errors.join( ‘

‘ ))); } return resolve({ stats, warnings: messages.warnings, }); }); }); } build();

그 다음엔, 웹팩에서 APP_ENV 를 인식 할 수 있도록, config/env.js 파일의 getClientEnvironment 함수에서 다음과 같이 APP_ENV 값을 넣어주세요.

config/env.js – getClientEnvironment

function getClientEnvironment ( publicUrl ) { const raw = Object .keys(process.env) .filter(key => REACT_APP.test(key)) .reduce( (env, key) => { env[key] = process.env[key]; return env; }, { NODE_ENV: process.env.NODE_ENV || ‘development’ , PUBLIC_URL: publicUrl, APP_ENV: process.env.APP_ENV || ‘browser’ } );

NPM 스크립트 생성

방금 만든 서버용 빌드스크립트를 실행시키는 NPM 스크립트를 만들어주겠습니다. package.json 의 script 부분을 다음과 같이 수정하세요.

package.json

“scripts” : { “start” : “cross-env NODE_PATH=src node scripts/start.js” , “start:server” : “node server” , “build” : “cross-env NODE_PATH=src node scripts/build.js” , “build:server” : “cross-env NODE_PATH=src node scripts/build.server.js” , “test” : “node scripts/test.js –env=jsdom” },

서버를 실행하는 start:server 스크립트와, 서버용 빌드를 만드는 build:server 스크립트를 생성하였습니다.

이제 yarn build:server 를 실행하여 서버용 빌드를 진행하세요.

$ yarn build:server

작업이 완료되엇으면, server/render 경로에 render.js 파일이 생성되었는지 확인하세요. 잘 되었다면, 서버쪽 코드를 본격적으로 작성해봅시다!

3-3. 서버쪽 코드 작성하기

서버사이드 렌더링 미들웨어 작성

우선, 서버사이드 렌더링을 위한 미들웨어를 작성하겠습니다.

server/render/index.js

const fs = require ( ‘fs’ ); const path = require ( ‘path’ ); const render = require ( ‘./render’ ).default; const template = fs.readFileSync(path.join(__dirname, ‘../../build/index.html’ ), { encoding: ‘utf8’ }); module .exports = (ctx) => { const location = ctx.path; const rendered = render(location); const page = template.replace( ‘

‘ , `

${rendered}

` ); ctx.body = page; }

이 코드는, html 파일을 불러온 다음에, 해당 파일의 루트 엘리먼트가 위치한 곳에 렌더링된 문자열을 넣어주고 이를 반환해줍니다.

그리고, module.exports 를 통해 이 파일을 불러와서 사용 할 수 있도록 했지요.

서버사이드 렌더링 미들웨어 사용

server/index.js

const Koa = require ( ‘koa’ ); const serve = require ( ‘koa-static’ ); const path = require ( ‘path’ ); const fs = require ( ‘fs’ ); const app = new Koa(); const render = require ( ‘./render’ ); app.use((ctx, next) => { if (ctx.path === ‘/’ ) return render(ctx); return next(); }); app.use(serve(path.resolve(__dirname, ‘../build/’ ))); app.use(render); app.listen( 3001 );

방금 만든 미들웨어를 불러와서, 기존에 Hello World 가 있던 자리에 넣어주었습니다. 그리고, 경로가 / 일때도, 서버사이드 렌더링을 하도록 설정했습니다. (koa-static 미들웨어가 먼저 실행되서 index.html 이 기본적으로 반환되었기 때문입니다)

작업을 마치셨다면, 서버를 재시작해보세요. 아까전에 start:server 를 만들었으니, 다음 명령어로 실행하시면 됩니다.

$ yarn start:server

그리고, Postman 으로 http://localhost:3001/ 에 웹요청을 해보세요.

서버사이드 렌더링이 성공적으로 되었습니다!

하지만 아직 끝이 아닙니다. 지금은 서버사이드 렌더링이 동기적으로 되고있기 때문에 동시에 하나의 요청밖에 처리하질 못합니다. 추가적으로, 아직 데이터 로딩이 이뤄지지 않았습니다. 다음 섹션에선 프로젝트에 리덕스를 적용하고 REST API 를 통하여 데이터를 불러오는걸 서버쪽에서도 이뤄지게끔 구현을 해보겠습니다.

3-4. Redux 적용하기

의존 모듈 설치

프로젝트에 redux 를 적용하기 위해 필요한 의존 모듈을 설치하세요. 웹요청도 할 것이기에, axios 도 설치하세요. 비동기 액션 관리는 redux-pender 를 사용하도록 하겠습니다.

$ yarn add redux react-redux redux-actions redux-pender axios

api.js 작성

우리는 테스트를 위하여 JSONPlaceholder 의 테스트용 REST API 를 사용하겠습니다.

lib 디렉토리에 api.js 파일을 다음과 같이 작성하세요.

src/lib/api.js

import axios from ‘axios’ ; export const getUsers = () => axios.get( ‘https://jsonplaceholder.typicode.com/users’ );

users 모듈 작성

리덕스의 디렉토리 구조는 Ducks 구조를 사용하도록 하겠습니다 (액션, 액션생성자, 리듀서를 모두 한 파일에 넣고 관리하는 구조이죠)

src/redux/modules/users.js

import { createAction, handleActions } from ‘redux-actions’ ; import { pender } from ‘redux-pender’ ; import * as api from ‘lib/api’ ; const GET_USERS = ‘users/GET_USERS’ ; export const getUsers = createAction(GET_USERS, api.getUsers); const initialState = { data: [] }; export default handleActions({ …pender({ type: GET_USERS, onSuccess: (state, action) => { return { data: action.payload.data } } }) }, initialState);

리듀서 합치기

우리는 redux-pender 를 사용하니, penderReducer 와 방금 만든 users 리듀서를 combineReducers 를 사용하여 합쳐주겠습니다.

src/redux/modules/index.js

import { combineReducers } from ‘redux’ ; import users from ‘./users’ ; import { penderReducer } from ‘redux-pender’ ; export default combineReducers({ users, pender: penderReducer });

configureStore.js 만들기

redux 의 스토어가 이제 서버쪽에서 생성 될 수도 있고, 클라이언트쪽에서 생성 될 수도 있으니, 스토어를 생성하는 함수를 따로 만들어서 파일로 저장하겠습니다.

src/redux/configureStore.js

import { createStore, applyMiddleware, compose } from ‘redux’ import penderMiddleware from ‘redux-pender’ ; import modules from ‘./modules’ ; const isDevelopment = process.env.NODE_ENV === ‘development’ ; const composeEnhancers = isDevelopment ? ( window .__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose) : compose; const configureStore = (initialState) => { const store = createStore(modules, initialState, composeEnhancers( applyMiddleware(penderMiddleware()) )); if ( module .hot) { module .hot.accept( ‘./modules’ , () => { const nextRootReducer = require ( ‘./modules’ ).default; store.replaceReducer(nextRootReducer); }); } return store; } export default configureStore;

그리고, 핫 리로딩이 제대로 작동하게 하기 위해서, store.js 라는 파일을 따로 생성해주겠습니다. 만약에, configureStore 호출하는것을 Root.js 를 하게되면, 코드가 다시 불러와질때마다 새 스토어가 만들어지므로 이렇게 파일을 따로 생성합니다. 이 파일은, 클라이언트쪽에서만 사용합니다. 추후, 이 파일에서 서버쪽에서 initialState 를 받아와서 생성하는 작업을 진행하겠습니다.

redux/store.js

import configureStore from ‘./configureStore’ ; export default configureStore();

어플리케이션에 리덕스 연동 (클라이언트)

이제 어플리케이션의 클라이언트 사이드에서 리덕스와 연동을 하겠습니다. Root 컴포넌트에서 BrowserRouter 내부에서 App 컴포넌트를 Provider 로 감싸시면 됩니다.

src/client/Root.js

import React from ‘react’ ; import { BrowserRouter } from ‘react-router-dom’ ; import App from ‘shared/App’ ; import store from ‘redux/store’ ; import { Provider } from ‘react-redux’ ; const Root = () => ( < BrowserRouter > < Provider store = {store} > < App /> ); export default Root;

자, 이제 리덕스 연동은 어느정도 끝났습니다. 다음 섹션에서는 클라이언트에서도, 서버쪽에서도 작동하는 데이터로딩을 구현하겠습니다.

3-5. 데이터 로딩 (클라이언트)

서버사이드 렌더링을 하면서, 데이터 로딩을 하는것은, 여러가지 방법이 있고 딱 정해진 방법은 없습니다. 데이터 로딩 관련 로직을 서버와 클라이언트 따로따로 작성하는 방식도 있고, 한번 작성해서 로직을 공유하는 방식도 있습니다.

우리는, react-router-server 에 포함된 도구들을 사용하여, 최대한 컴포넌트의 복잡도를 줄이면서도, 컴포넌트쪽에서 코드를 한번 작성하고 서버쪽에서는 별도의 작업없이 그 코드를 사용하게하고, 또 리덕스와 호흡을 잘 맞추면서 구현을 해보도록 하겠습니다.

먼저 react-router-server 를 설치하세요.

$ yarn add react-router-server

Users 라우트 생성

유저목록을 불러와서 렌더링해주는, Users 라우트를 생성하겠습니다. 데이터를 로딩 할 때, componentDidMount 가 아닌, componentWillMount 에서 작업을 호출하는데요, 그 이유는 componentWillMount 는 서버쪽에서도 호출이 되기 때문입니다. (componentDidMount 는 서버쪽에선 실행되지 않습니다) 그리고, react-router-server 에서 제공된 withDone 함수를 불러와서 사용하였는데요, 리덕스의 connect 함수처럼, 컴포넌트를 내보내기전에 한번 감싸주면 됩니다.

그렇게하면, 컴포넌트에 done 이란 함수가 props 로 전달이 되는데, 이 함수가 호출 될 때 까지 렌더링을 지연시킵니다. 따라서, 서버렌더링을 할 때 다음과 같은 흐름으로 진행이 됩니다:

componentWillMount 실행 → 데이터로딩 → done 함수가 호출 될 때까지 렌더링을 지연시킴 → done 이 호출됨 → render 실행

클라이언트에서는 렌더링이 지연되지 않습니다:

componentWillMount 실행 → 데이터 로딩 → 데이터 로딩중에 render 됨 → 값이 업데이트됨 → 다시 render 하여 업데이트

우리는, 컴포넌트를 리덕스에 연결시키고, componentWillMount 에서 데이터가 비어있을때만 요청을 하도록 했습니다. 그 이유는, 서버사이드 렌더링된 결과물을 브라우저가 받고 난 다음에, 컴포넌트 라이프싸이클은 똑같이 흐르기 때문입니다. 데이터가 이미 있는데 굳이 중첩적으로 요청을 할 필요는 없겠지요? 이를 방지하기위해 저렇게 조건을 넣었습니다.

UsersActions.getUsers 가 호출되면, Promise 를 반환하는데요, 이게 성공하거나 실패 했을 시, done 을 호출합니다. 서버쪽에선, 그 다음에 데이터가 생긴상태에서 첫 렌더링을 하니, 우리가 원하는 데이터가 제대로 나타나겠지요.

src/pages/Users.js

import React, { Component } from ‘react’ ; import { connect } from ‘react-redux’ ; import { bindActionCreators } from ‘redux’ ; import * as usersActions from ‘redux/modules/users’ ; import { withDone } from ‘react-router-server’ ; class Users extends Component { componentWillMount() { const { UsersActions, data, done } = this .props; if (data.length !== 0 ) return false ; UsersActions.getUsers().then(done, done); } render() { const { data } = this .props; const userList = data.map( user => < li key = {user.id} > {user.name} ); return ( < div > < ul > {userList} ); } } export default withDone(connect( (state) => ({ data: state.users.data }), (dispatch) => ({ UsersActions: bindActionCreators(usersActions, dispatch) }) )(Users));

Users 라우트 적용

작업이 완료되었다면, 페이지 인덱스에 Users 를 추가하고, 라우트도 설정한다음에 메뉴에서도 연결시키세요.

src/pages/index.js

export { default as Home } from ‘./Home’ ; export { default as About } from ‘./About’ ; export { default as Posts } from ‘./Posts’ ; export { default as Post } from ‘./Post’ ; export { default as Users } from ‘./Users’ ;

src/pages/index.async.js

import asyncRoute from ‘lib/asyncRoute’ ; export const Home = asyncRoute(() => import ( ‘./Home’ )); export const About = asyncRoute(() => import ( ‘./About’ )); export const Post = asyncRoute(() => import ( ‘./Post’ )); export const Posts = asyncRoute(() => import ( ‘./Posts’ )); export const Users = asyncRoute(() => import ( ‘./Users’ ));

src/shared/App.js

import React, { Component } from ‘react’ ; import { Route, Switch } from ‘react-router-dom’ ; import { Home, About, Posts, Users } from ‘pages’ ; import Menu from ‘components/Menu’ ; class App extends Component { render() { return (

); } } export default App;

src/components/Menu.js – render – ul

  • < NavLink to = "/users" activeStyle = {activeStyle} > Users

    위 코드를

    내부에 추가하세요.

    이렇게 하고 난 다음엔, 브라우저로 개발서버에 들어가서 Users 페이지를 들어가보세요.

    잘 되었나요? 그러면 이제 서버사이드에서도 렌더링을 해보겠습니다.

    3-6. 데이터 로딩 (서버)

    이제, 서버 렌더링 쪽에서 데이터 로딩도 제대로하고, 렌더링을 비동기로 작동하게 하기 위해서, react-router-server 의 renderToString 을 사용하도록 하겠습니다. 기존의 ReactDOMServer.renderToString 은 이 파일에서 더 이상 사용하지 않으니 해당 코드를 지워주셔도 됩니다.

    render.js 를 다음과 같이 수정하세요.

    src/server/render.js

    import React from ‘react’ ; import { StaticRouter } from ‘react-router’ ; import App from ‘shared/App’ ; import configureStore from ‘redux/configureStore’ ; import { Provider } from ‘react-redux’ ; import { renderToString } from ‘react-router-server’ ; const render = async (location) => { const store = configureStore(); const { html } = await renderToString( < StaticRouter location = {location} > < Provider store = {store} > < App /> ); // 스토어와, 렌더링된 문자열 결과물을 반환합니다 return { html, state: store.getState() }; } export default render;

    render 함수가 호출 될 때마다 store 를 생성하도록 했습니다. 만약에 스토어를 하나만 만들어서 사용한다면, 요청마다 상태가 초기화되지 않고 유지가 되어서 상태를 공유하게되는 현상이 발생하기 때문입니다.

    renderToString 은 Promise 를 반환하기 때문에 async , await 문법을 통해 해당 작업을 함수 내에서 비동기식으로 기다리도록 하였습니다.

    그리고, 렌더링을 하고 난 다음엔 html 문자열 뿐만 아니라 현재의 상태를 클라이언트에게 전달을 해주어야 하므로, store.getState() 값을 html 와 같이 객체에 넣어서 리턴하여 서버쪽에서 사용 할 수 있게 해주었습니다.

    작업을 완료하셨으면, 다음명령어들을 통하여 빌드하세요. (클라이언트쪽 코드도 바뀌었으니 다시 빌드합니다.)

    $ yarn build:server $ yarn build

    이제 render 함수는 문자열을 리턴하는게 아니라 store 상태와 함께 객체를 리턴합니다. 이제 이에 맞춰서, 서버쪽 코드를 수정해야합니다.

    html 값은 root 엘리먼트안에 넣고, 그 다음 부분에 ` ); ctx.body = page; } ); }

    만약에 Node v7 이상 버전을 사용한다면 async await 을 사용하여 다음과 같이 작성 할 수도 있습니다.

    module .exports = async (ctx) => { const location = ctx.path; const { html, state } = await render(location); const page = template.replace( '

    ' , `

    ${html}

    ` ); ctx.body = page; }

    이제, 서버를 다시 시작하고 Postman 을 통하여 http://localhost:3001/users 에 GET 요청을 해보세요.

    이제, 클라이언트에서 window.__PRELOADED_STATE__ 를 리덕스 스토어의 초기 상태로 지정해주면되는데요, 이렇게 JSON.stringify 만 사용하면, 보안적으로 취약 할 수 있습니다. 만약에 상태에 태그를 닫고, 악성스크립트가 들어있는 문자열이 있다면 그게 그대로 실행될수도 있기 때문이죠.

    따라서, 보안을 위하여 JSON 객체를 안전하게 변환 하여 사용 할 수 있게 해주는 serialize-javascript 를 설치해서 사용하도록 하겠습니다. 이 라이브러리는, JSON 을 문자열로 변환하는 과정에서 문자열을 이스케이핑 해주어 스크립트 태그가 닫히지 않게 해주고, 또 정규식과 함수도 문자열로 전달 할 수 있게 해줍니다.

    $ yarn add serialize-javascript

    설치 후에는, JSON.stringify 대신에 serialize-javascript 를 불러와서 사용해주면 됩니다.

    server/render/index.js

    const fs = require ( 'fs' ); const path = require ( 'path' ); const render = require ( './render' ).default; var serialize = require ( 'serialize-javascript' ); const template = fs.readFileSync(path.join(__dirname, '../../build/index.html' ), { encoding: 'utf8' }); module .exports = (ctx) => { const location = ctx.path; return render(location).then( ({html, state}) => { const page = template.replace( '

    ' , `

    ${html}

    ` ); ctx.body = page; } ); }

    이제 서버쪽 코드는 어느정도 마무리가 되었습니다. 클라이언트에서 방금 전달해준 리덕스 상태를 받아오게 코드를 작성해봅시다.

    리덕스 초기상태 지정하기

    리덕스 초기상태를 넣어주는것은 매우 간단합니다. 그냥, store.js 파일에서 configureStore 의 인자로 window.__PRELOADED_STATE__ 을 넣으세요.

    src/redux/store.js

    import configureStore from './configureStore' ; export default configureStore( window .__PRELOADED_STATE__);

    이제 서버사이드 렌더링은 거의 끝났습니다! 렌더링도 잘 되고 데이터 로딩도 잘 됩니다. 마지막으로, SEO 에 있어서 정말 중요한 페이지 타이틀 제목 및 meta 태그 설정 방법과, 이를 서버렌더링쪽에서도 하는 방법을 알아보겠습니다.

    3-7. react-helmet 을 통한 페이지 헤드 정보 설정

    리액트로 만든 어플리케이션의 페이지에 페이지 제목과 meta 태그를 설정하는것은, 리액트에서 관리되는것이 아니기에, 다음과 같은 형식의 코드를 직접 입력해야하죠.

    document .title = 'something' ; var meta = document .createElement( 'meta' ); meta.httpEquiv = "X-UA-Compatible" ; meta.content = "IE=edge" ; document .getElementsByTagName( 'head' )[ 0 ].appendChild(meta);

    react-helmet 소개

    각 라우트마다 이렇게 설정하는것은, 보기에도 그렇게 좋지 않고, 번거로울 수도 있습니다. 이 작업을 매우 용이하게 해주는 라이브러리가있는데요, 바로 react-helmet 입니다. 이 라이브러리는 페이지의 head 설정을 컴포넌트 렌더링하듯이 JSX 에서 할 수 있게 해주는 아주 유용한 라이브러리입니다.

    이 라이브러리를 사용하면, 다음과 같은 형식으로 head 설정을 할 수 있게 됩니다.

    import React from "react" ; import {Helmet} from "react-helmet" ; class Application extends React . Component { render () { return (

    My Title ...

    ); } };

    Helmet 을 통해 설정한 값들은 DOM 트리의 더 깊숙히 위치하는것이 우선권을 가집니다. 예를들어, 다음과 같은 코드가 있다면,

    < Helmet > < title > My Title < meta name = "description" content = "Helmet application" /> < Child > < Helmet > < title > Nested Title < meta name = "description" content = "Nested component" />

    페이지의 타이틀은 Nested Title 으로 설정됩니다.

    설치와 사용

    이제 이 라이브러리를 설치하고 적용을 해보겠습니다.

    $ yarn add react-helmet

    설치를 하고나서, 먼저 App 컴포넌트에서 사용해봅시다.

    src/shared/App.js

    import React, { Component } from 'react' ; import { Route, Switch } from 'react-router-dom' ; import { Home, About, Posts, Users } from 'pages' ; import { Helmet } from "react-helmet" ; import Menu from 'components/Menu' ; class App extends Component { render() { return (

    React Router & SSR

    ); } } export default App;

    타이틀이 설정 되었습니다!

    이제, About 페이지에도 타이틀 설정을 해주겠습니다.

    src/pages/About.js

    import React from 'react' ; import queryString from 'query-string' ; import { Helmet } from 'react-helmet' ; const About = ({location, match}) => { const query = queryString.parse(location.search); const detail = query.detail === 'true' ; const { name } = match.params; return ( < div > < Helmet > < title > {`About ${name ? name : ''}`} < h2 > About {name} {detail && 'detail: blahblah'} ); }; export default About;

    Helmet 을 사용하실 때 주의 하실 점은, 내용을 하나의 문자열로 해야 한다는 점 입니다. 만약에 예를들어 여러분이 About {name} 이런식으로 하게 된다면, 실제로 이 받게되는 children 은 ["About ", name] 형태의 배열인데 Helmet 에서 이를 허용하지 않습니다. 따라서 내용에 변수를 넣어야 할 때는 전체를 {} 로 감싸서 넣어주셔야합니다.</p> <p>이제, About 페이지도 타이틀이 제대로 붙었는지 확인해보세요.</p> <p>;</p> <p>더 내부에 있는 Helmet 정보가 우선순위를 가져서 About 페이지의 타이틀이 제대로 나타났습니다.</p> <p>서버사이드 렌더링</p> <p>타이틀 및 메타정보 설정은 검색엔진 최적화를 위하여 해주는 작업인데, 서버사이드 렌더링이 빠질 수 없지요.</p> <p>Helmet 정보를 서버쪽에 전달 해 줄때는, Helmet.renderStatic 함수를 사용합니다. 이 함수를 실행하여 만들어진 인스턴스는 다음 값들을 지니고있습니다:</p> <p>base</p> <p>bodyAttributes</p> <p>htmlAttributes</p> <p>link</p> <p>meta</p> <p>noscript</p> <p>script</p> <p>style</p> <p>title</p> <p>서버 렌더용 코드를 다음과 같이 수정하세요:</p> <p>src/server/render.js</p> <p>import React from 'react' ; import { StaticRouter } from 'react-router' ; import App from 'shared/App' ; import configureStore from 'redux/configureStore' ; import { Provider } from 'react-redux' ; import { renderToString } from 'react-router-server' ; import { Helmet } from 'react-helmet' ; const render = async (location) => { const store = configureStore(); const { html } = await renderToString( < StaticRouter location = {location} > < Provider store = {store} > < App /> </ Provider > </ StaticRouter > ); // helmet 정보를 가져옵니다 const helmet = Helmet.renderStatic(); // 스토어 상태와, 렌더링된 문자열 결과물, 그리고 helmet 정보를 반환합니다 return { html, state: store.getState(), helmet }; } export default render;</p> <p>helmet 정보를 렌더링된 문자열과, 스토어 상태와 함께 전달해주었습니다.</p> <p>helmet 태그들도, 우리가 렌더링된 문자열을 넣어주었던것처럼 html 코드에 삽입해주어야하는데요, 우선 삽입 할 위치를 지정해줍시다.</p> <p>public 디렉토리의 index.html 을 열어서 <head> 태그의 맨 윗부분에 다음 코드를 넣어주세요.</p> <p>public/index.html</p> <p><!doctype html> <html lang="en"> <head> <meta helmet> (...)</p> <p>그리고 <head> 의 하단에 있는 <title>React App 을 지우세요. html 을 수정하고난다음엔, 다시 코드를 빌드하세요.

    $ yarn build $ yarn build:server

    Helmet 정보 html 에 삽입하기

    helmet 객체 안에있는 title, meta, link 를 toString() 을 해주면 태그형태로 문자열변환이 됩니다. 을 helmet 태그들로 치환하세요.

    server/render/index.js

    const fs = require ( 'fs' ); const path = require ( 'path' ); const render = require ( './render' ).default; var serialize = require ( 'serialize-javascript' ); const template = fs.readFileSync(path.join(__dirname, '../../build/index.html' ), { encoding: 'utf8' }); module .exports = (ctx) => { const location = ctx.path; return render(location).then( ({html, state, helmet}) => { const page = template.replace( '

    ' , `

    ${html}

    ` ) .replace( '' , ` ${helmet.title.toString()} ${helmet.meta.toString()} ${helmet.link.toString()} ` ); ctx.body = page; } ); }

    서버를 실행하고, Postman 으로 http://localhost:3001/ 에 요청을 해보세요.

    title 이 잘 나타났나요?

    마치면서

    수고하셨습니다. 리액트 라우터를 통해 여러 페이지를 관리하는것부터 시작해서, 코드스플리팅, 그리고 대망의 서버사이드 렌더링까지, 성공적으로 해내셨습니다.

    프로젝트에 코드스플리팅과 서버사이드 렌더링까지 붙고나면, 구조가 조금은 복잡해집니다. 이 두가지 기술은 프로젝트 개발에 있어서 필수사항은 아니지만, 경우에 따라 구현해놓으면 더 좋을 수도 있습니다. 하지만, 프로젝트 개발을 할 때, 이 작업들은 프로젝트를 마무리하는 시점에서 진행하기를 권고합니다. 그 이유는, 이 작업은 시간이 좀 들어가는 작업이고, 이 때문에 프로젝트 개발이 지연될 수 있기 때문입니다. 기능 개발이 더 우선순위이기 때문에, 여러분의 어플리케이션이 작동할 수 있게 만든 그 다음작업으로 해도 상관없습니다.

    이번에 이렇게 적용을 해보았고, 원리와 방식을 이해하면, 추후에 여러분의 프로젝트에서도 적용 할 수 있을겁니다. 이 두가지 기술은 다가가기엔 어려워보일수도있지만 이해를 하고나면 생각보다 그렇게 어렵지는 않습니다. 그리고 이 두가지는 프로젝트 개발에 있어서 필수사항은 아닙니다. 하지만 경우에 따라 필요해 질 수도 있지요.

  • [ReactJS] 서버사이드 렌더링(SSR)과 클라이언트사이드 렌더링(CSR)이란

    반응형

    랜더링이란 ( Rendering)

    서버로 부터 받은 내용을 브라우저에 표시함

    - Loader 가 서버로 부터 원본 HTML 문서를 읽어들임

    - 브라우저는 파싱을 통해 최종적으로 어떤 내용을 페이지에 렌더링할 지 결정한다.

    1) DOM 트리 구축

    2) CSSSOM 트리 구축

    3) JavaScript 실행

    - 렌더 트리를 구축

    - CSS 설정/레이아웃 위치 지정 후 렌더링 트리가 페인팅된다.

    DOM(Document Object Model) : HTML 요소들의 구조화된 표현

    CSSOM(Cascading Style Sheets Object Model) : 요소들과 연관된 스타일 정보의 구조화된 표현

    * DOM이란 무엇인가

    seizemymoment.tistory.com/133?category=879018

    서버사이드 렌더링(Server Side Rendering )

    서버 연산을 통해서 렌더링하고 완성된 페이지 형태 로 응답한다.

    페이지 이동 시 새로운 페이지를 요청한다.

    (장점)

    - 검색엔진에 최적화

    - HTML이 먼저 렌더링 되기 때문에 성능이 개선된다.

    (단점)

    - 페이지 요청마다 새로고침이 일어나기 때문에 페이지 이동시 화면이 깜빡 거린다.

    - 서버 렌더링에 따른 부하가 발생

    클라이언트 사이드 렌더링 (Client Side Rendering)

    클라이언트에서 렌더링하는 방식이다.

    서버로 부터 데이터를 받아

    클라이언트에서 바뀐 데이터가 있는 화면만 새롭게 랜더링 함

    첫 요청 시에는 한 페이지만 불러옴(reactJS에서는 index.js)

    (장점)

    - 리로딩 없는 빠른 인터렉션

    - 안드로이드, IOS, 웹 모바일 등 다양한 플랫폼이 서버를 공유할 수 있게 되어

    효율적으로 서버를 운영할 수 있게 된다.

    (단점)

    - 초기 구동속도가 느리다

    - 검색엔진 최적화가 어렵다.

    velog.io/@ash3767/%EC%84%9C%EB%B2%84%EC%82%AC%EC%9D%B4%EB%93%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EC%82%AC%EC%9D%B4%EB%93%9C-%EB%A0%8C%EB%8D%94%EB%A7%81

    velog.io/@wooder2050/%EB%A6%AC%EC%95%A1%ED%8A%B8React%EB%8A%94-%EC%99%9C-%EC%93%B0%EB%8A%94-%EA%B1%B4%EB%8D%B0

    반응형

    React 서버사이드 렌더링 가이드 (번역)

    React 서버사이드 렌더링 가이드 (번역)

    팀 내에서 React SSR (Server Side Rendering)을 도입해보자는 논의가 오가게 되었다. 그래서 자료를 찾던 중 예전(20.12.8)에 작성된 글이긴 하지만 간단하게 express server(Node.js) 를 띄워 SSR 을 해보는 가이드 글이 있어 번역을 한번 해보며 공부해보려고 한다.

    해당 글에서는 renderToString 을 사용하였는데 react 18 이 되며 suspense 및 streaming에 대한 권장으로 renderToPipeableStream 도 추가되었는데 해당 부분은 react 릴리즈 노트 등을 참고하자.

    | 해당 내용은 개인적인 공부를 위한 글로 오역 및 개인적인 의견이 반영된 내용이 있을 수 있으니 참고하여 주시기 바라며 문제가 되는 내용이 있는 경우 메일로 피드백 부탁합니다.

    대부분의 웹 애플리케이션은 서버에 데이터만 요청한다. 모든 HTML 생성은 클라이언트 측에서 수행되며 사용자가 링크를 클릭할 때마다 서버에 요청하여 받은 데이터로 HTML을 생성하게 된다.

    초기 렌더링의 경우 전체 애플리케이션에 대한 Javascript 및 CSS를 보내는 것이 아닌 초기 HTML과 일부의 Javascript, CSS 등을 보낸 후 클라이언트 측에서 Javascript 로드 이후 페이지를 렌더링하게 된다.

    그리고 브라우저 새로고침이 발생하지 않도록 History API 를 사용하여 URL을 변경하고 페이지 새로고침 없이 클라이언트에서 HTML을 렌더링하도록 한다.

    만약 example.com/ 또는 example.com/settings 와 같이 브라우저에 URL을 직접 액세스 하는 경우 모든 페이지에 대해서 서버는 동일한 HTML 및 리소스를 다시 보내게 된다. 클라이언트에서는 브라우저 URL을 읽고 지정된 경로에 맞게 페이지를 렌더링하게 된다.

    < html lang = " en " > < head > < meta charset = " UTF-8 " /> < meta name = " viewport " content = " width=device-width,initial-scale=1 " /> < meta http-equiv = " X-UA-Compatible " content = " ie=edge " /> < title > SPA Application < meta name = " description " content = " My SPA website. " /> < link rel = " icon " href = " /assets/favicon.ico " /> < link href = " styles.css " rel = " stylesheet " /> < body > < div id = " app " > < script src = " vendor.js " > < script src = " main.js " >

    예로 위의 HTML을 내려받은 후 Javascript에 의해

    컨테이너에 요소를 채워 넣는다.

    SPA Benefits

    네이티브 애플리케이션과 비슷한 경험을 제공할 수 있으며 HTML이 클라이언트에서 생성되기 때문에 페이지 전환이 빠르다. Javascript 및 CSS 파일과 같은 리소스는 한번만 로드되고 응용 프로그램이 로드되면 다시 요청하지 않기 때문에 서버 대역폭을 절약 할 수 있다. 초기 애플리케이션 로드 후 서버에 킬로바이트 정도의 작은 데이터 요청만 한다. 서비스워커를 사용하여 사용자에게 더 나은 오프라인 경험을 제공하는 사용자 정의 화면을 표시 할 수 있다.

    SPA Pitfalls

    SPA는 애플리케이션 Javascript 및 CSS 파일을 한 번에 제공해야 하므로 초기 응용프로그램 로드 시간이 지체될 수 있다. 이 문제는 코드 스플리팅 등을 통해 초기 로드 시간을 개선할 수도 있다. 하지만 로드 시간이 오래 걸리는 경우 사용자는 오랜 시간 동안 빈 화면을 보게 된다. SPA에서는 Javascript가 클라이언트에서 HTML을 생성 관리하기 때문에 무거운 작업을 클라이언트에서 수행하게 될 수도 있다. 서버에서 제공하는 초기 HTML에는 내용이 포함되어있지 않기 때문에 검색 엔진은 웹사이트를 상위 검색 결과에 노출하지 않을 수도 있다.

    위의 장점에서는 지금은 더 장점이 늘어난 부분도 있고 단점의 경우에는 해결 방법이 생겨 해결이 가능한 요소들도 존재한다. 여기서 우리는 초기 렌더링에 대한 부분을 SSR로 대체하여 초기 렌더링 시 사용자가 빈 화면을 보는 시간을 줄이고 검색 엔진 노출에 대한 부분을 해결해 보고자 한다.

    Setting up a React project

    간단한 React 애플리케이션을 설정해보자. create-react-app 과 같은 CLI 도구를 사용하여 React 프로젝트를 생성하거나 GitHub에서 표준 React 보일러 플레이트를 복제할 수 있지만 여기서는 custom webpack 프로젝트로 설정해 보겠다.

    Webpack 을 사용하여 Babel 의 도움으로 React 및 ES6를 Javascript로 변환한다. 그리고 CSS를 별도로 생성하여 styles.css 파일로 추출한다. 애플리케이션 Javascript는 main.js 와 vendor.js 로 분할된다.

    // babel.config.js module . exports = { presets : [ '@babel/env' , '@babel/react' ] , plugins : [ '@babel/plugin-transform-runtime' , '@babel/plugin-transform-async-to-generator' , '@babel/transform-arrow-functions' , '@babel/proposal-object-rest-spread' , '@babel/proposal-class-properties' , ] , }

    { "name" : "react-ssr" , "description" : "A React server-side rendering (SSR) sample project." , "version" : "1.0.0" , "scripts" : { "start" : "NODE_ENV=development webpack serve" , "build" : "NODE_ENV=production webpack" } , "dependencies" : { "react" : "^17.0.1" , "react-dom" : "^17.0.1" } , "devDependencies" : { "@babel/core" : "^7.12.9" , "@babel/plugin-proposal-class-properties" : "^7.12.1" , "@babel/plugin-proposal-object-rest-spread" : "^7.12.1" , "@babel/plugin-transform-arrow-functions" : "^7.12.1" , "@babel/plugin-transform-async-to-generator" : "^7.12.1" , "@babel/plugin-transform-runtime" : "^7.12.1" , "@babel/preset-env" : "^7.12.7" , "@babel/preset-react" : "^7.12.7" , "@babel/runtime" : "^7.12.5" , "babel-loader" : "^8.2.2" , "copy-webpack-plugin" : "^6.3.2" , "css-loader" : "^5.0.1" , "html-webpack-plugin" : "^4.5.0" , "mini-css-extract-plugin" : "^1.3.2" , "node-sass" : "^5.0.0" , "sass-loader" : "^10.1.0" , "webpack" : "^5.10.0" , "webpack-cli" : "^4.2.0" , "webpack-dev-server" : "^3.11.0" } }

    // src/components/app/app.jsx import React from 'react' // import child components import { Counter } from '../counter' // export entry application component export class App extends React . Component { constructor ( ) { console . log ( 'App.constructor()' ) super ( ) } // render view render ( ) { console . log ( 'App.render()' ) return ( < div className = " ui-app " > < Counter name = " Monica Geller " /> ) } }

    < html lang = " en " > < head > < meta charset = " UTF-8 " /> < meta name = " viewport " content = " width=device-width, initial-scale=1.0 " /> < meta http-equiv = " X-UA-Compatible " content = " ie=edge " /> < title > React Boilerplate / Webpack 4 / Babel 7 < meta name = " description " content = " React boilerplate with Webpack 4 and Babel 7 " /> < link rel = " icon " href = " /assets/favicon.ico " /> < body > < div id = " app " >

    // src/index.js import React from 'react' import ReactDOM from 'react-dom' // import App components import { App } from './components/app' // compile App component in `#app` HTML element ReactDOM . render ( < App / > , document . getElementById ( 'app' ) )

    // webpack.config.js const path = require ( 'path' ) const HTMLWebpackPlugin = require ( 'html-webpack-plugin' ) const MiniCssExtractPlugin = require ( 'mini-css-extract-plugin' ) const CopyWebpackPlugin = require ( 'copy-webpack-plugin' ) /*-------------------------------------------------*/ module . exports = { // webpack optimization mode mode : 'development' === process . env . NODE_ENV ? 'development' : 'production' , // entry files entry : [ './src/index.js' , // react ] , // output files and chunks output : { path : path . resolve ( __dirname , 'dist' ) , filename : 'build/[name].js' , } , // module/loaders configuration module : { rules : [ { test : / \. jsx ? $ / , exclude : / node_modules / , use : [ 'babel-loader' ] , } , { test : / \. scss $ / , use : [ MiniCssExtractPlugin . loader , 'css-loader' , 'sass-loader' ] , } , ] , } , // webpack plugins plugins : [ // extract css to external stylesheet file new MiniCssExtractPlugin ( { filename : 'build/styles.css' , } ) , // prepare HTML file with assets new HTMLWebpackPlugin ( { filename : 'index.html' , template : path . resolve ( __dirname , 'src/index.html' ) , minify : false , } ) , // copy static files from `src` to `dist` new CopyWebpackPlugin ( { patterns : [ { from : path . resolve ( __dirname , 'src/assets' ) , to : path . resolve ( __dirname , 'dist/assets' ) , } , ] , } ) , ] , // resolve files configuration resolve : { // file extensions extensions : [ '.js' , '.jsx' , '.scss' ] , } , // webpack optimizations optimization : { splitChunks : { cacheGroups : { default : false , vendors : false , vendor : { chunks : 'all' , // both : consider sync + async chunks for evaluation name : 'vendor' , // name of chunk file test : / node_modules / , // test regular expression } , } , } , } , // development server configuration devServer : { port : 8088 , historyApiFallback : true , } , // generate source map devtool : 'source-map' , }

    위의 설정에서 src/index.js 는 컴파일의 진입점이고 src/components/app 은 애플리케이션의 진입 컴포넌트이다. App 컴포넌트는 기본적으로 Counter 컴포넌트를 렌더링한다.

    처음 index.html 에는 비어있는

    요소만 있었고 위의 리액트 컴포넌트들에 의해 클라이언트에서 요소들이 생성된다.

    이는 개발 서버의 응답을 확인했을 때 비어있는

    요소가 표시되는 것을 확인할 수 있다. 이것은 검색 엔진 크롤러에 잡히지 않으므로 서버에서 요소를 적절한 HTML로 채워야 한다.

    Setting up an SSR server

    서버 측에서 리액트 렌더링을 하려면 Node.js 서버를 사용해야 한다. 샘플 프로젝트의 경우 Express.js 를 사용하여 HTTP 서버를 구성하였다. server/express.js 파일을 생성하고 웹 애플리케이션의 JS, CSS 및 기타 리소스 파일을 제외한 모든 경로에 대해 index.html 파일을 제공하는 로직을 작성해보자.

    server/ ├── express.js └── index.js

    // server/express.js const express = require ( 'express' ) const fs = require ( 'fs' ) const path = require ( 'path' ) // create express application const app = express ( ) // serve static assets app . get ( / \. ( js | css | map | ico ) $ / , express . static ( path . resolve ( __dirname , '../dist' ) ) ) // for any other requests, send `index.html` as a response app . use ( '*' , ( req , res ) => { // read `index.html` file let indexHTML = fs . readFileSync ( path . resolve ( __dirname , '../dist/index.html' ) , { encoding : 'utf8' , } ) // set header and status res . contentType ( 'text/html' ) res . status ( 200 ) return res . send ( indexHTML ) } ) // run express server on port 9000 app . listen ( '9000' , ( ) => { console . log ( 'Express server started at http://localhost:9000' ) } )

    // server/index.js require ( './express.js' )

    이제 $ node server/index.js 명령으로 index.html 을 제공하는 HTTP 서버를 실행할 수 있다.

    이제 서버에서 index.html 을 제공하기는 하지만 여전히

    요소는 비어있으며 서버 측 렌더링 코드를 작성해야 한다.

    브라우저의 경우 src/index.js 파일을 진입점으로 App 컴포넌트를 가져와 내부에서 렌더링을 수행한다.

    // import App components import { App } from './components/app' // compile App component in `#app` HTML element ReactDOM . render ( < App /> , document . getElementById ( 'app' ) )

    위와 같은 수행을 서버에서 동일하게 해야 하며 index.html 을 제공하는 동안

    요소를 HTML로 채워야 한다.

    react-dom/server 패키지는 ReactDOM.render 와 마찬가지로 컴포넌트를 렌더링하는 renderToString()을 제공하지만 DOM 요소를 채우는 대신 HTML 문자열을 반환한다. 브라우저에 응답을 반환하기 전에 server/express.js 에서 HTML을 채워보도록 하자.

    // server/express.js const express = require ( 'express' ) const fs = require ( 'fs' ) const path = require ( 'path' ) const React = require ( 'react' ) const ReactDOMServer = require ( 'react-dom/server' ) // create express application const app = express ( ) // import App component const { App } = require ( '../src/components/app' ) // serve static assets app . get ( / \. ( js | css | map | ico ) $ / , express . static ( path . resolve ( __dirname , '../dist' ) ) ) // for any other requests, send `index.html` as a response app . use ( '*' , ( req , res ) => { // read `index.html` file let indexHTML = fs . readFileSync ( path . resolve ( __dirname , '../dist/index.html' ) , { encoding : 'utf8' , } ) // get HTML string from the `App` component let appHTML = ReactDOMServer . renderToString ( < App / > ) // populate `#app` element with `appHTML` indexHTML = indexHTML . replace ( '

    ' , `

    ${ appHTML }

    ` ) // set header and status res . contentType ( 'text/html' ) res . status ( 200 ) return res . send ( indexHTML ) } ) // run express server on port 9000 app . listen ( '9000' , ( ) => { console . log ( 'Express server started at http://localhost:9000' ) } )

    // server/index.js const path = require ( 'path' ) // ignore `.scss` imports require ( 'ignore-styles' ) // transpile imports on the fly require ( '@babel/register' ) ( { configFile : path . resolve ( __dirname , '../babel.config.js' ) , } ) // import express server require ( './express.js' )

    express.js 에서 App 컴포넌트를 가져와 브라우저에 렌더링할 HTML 문자열로 채우게 된다. 여기에서 React 컴포넌트를 가져와 JSX 표현 식도 사용하게 되는데 babel 을 사용하여 변환해야 한다.

    $ npm i -D @babel/register ignore-styles

    위와 같은 설정을 끝낸 이후에는 서버에서 미리 렌더링 된 HTML을 응답 받기를 기대해야 한다. 실제로 요청 시에 HTML 응답에서

    가 채워져 있는 것을 볼 수 있으며 검색 엔진 크롤러가 웹사이트를 크롤링할 때 빈 페이지로 인식하지 않게 된다.

    이제 프론트에서 몇 가지 수정이 필요한데 지금 당장은 프론트에서는

    가 채워져 있는 것을 신경 쓰지 않고 재 렌더링이 발생할 것이다.

    서버 측에서 제공된 HTML을 재사용하고 해당 DOM 요소에 이벤트 리스너를 연결하는 작업이 필요하다. 이러한 작업을 hydration 이라고 하며 ReactDOM.render 는 hydration 을 수행하지 않지만 ReactDOM.hydrate 를 사용하여 hydration 을 수행할 수 있다.

    // import App components import { App } from './components/app' // compile App component in `#app` HTML element ReactDOM . hydrate ( < App /> , document . getElementById ( 'app' ) )

    ReactDOM.hydrate 는 ReactDOM.render 와 동일하게 동작하지만 서버 측에서 전달받은 HTML을 사용한다. 서버에서 전달받은 HTML이 렌더링할 App 컴포넌트와 동일할 것으로 예상하고 동작하기 때문에 불일치하는 경우에 문제가 발생할 수 있다. (자세한 내용은 설명서를 참고)

    개발 모드에서는 개발 서버가 서버 측 렌더링을 수행하지 않기 때문에 ReactDOM.render 가 필요하다. render 와 hydrate 호출이 있는 index.dev.js 와 index.prod.js 를 나누고 webpack.config.js 내부에서 환경변수를 사용하여 진입점을 설정하자.

    Handling Routing

    라우팅은 URL 경로에 각 페이지를 표시하는 방법이다. 예전에는 서버에서 라우팅이 수행되었지만 SPA 에서는 클라이언트 측에서 라우팅 메커니즘을 사용한다. 브라우저의 URL 경로가 변경되면 이전의 컴포넌트를 제거하고 새로운 컴포넌트를 마운트하여 페이지가 변경되는 것 같은 경험을 제공한다.

    React 를 사용한 웹 프로젝트에서 라우터는 react-router-dom 을 사용한다. 예제로 '/' 기본 경로에는 Counter 컴포넌트를 렌더링하고 '/post' 경로에는 Post 컴포넌트를 렌더링해보자. 클라이언트 라우터를 변경하기 위해 react-router-dom 패키지를 설치하여 시작하자.

    $ npm install -D react-router-dom

    Switch 및 Route 컴포넌트를 설정하고 BrowserRouter 컴포넌트를 App 컴포넌트에 래핑한다.

    // src/components/app/app.component.jsx import React from 'react' import { NavLink as Link , Switch , Route } from 'react-router-dom' // import child components import { Counter } from '../counter' import { Post } from '../post' // export entry application component export class App extends React . Component { constructor ( ) { console . log ( 'App.constructor()' ) super ( ) } // render view render ( ) { console . log ( 'App.render()' ) return ( < div className = " ui-app " > { /* navigation */ } < div className = " ui-app__navigation " > < Link className = "ui-app__navigation__link" activeClassName = "ui-app__navigation__link--active" to = "/" exact = { true } > Counter < Link className = "ui-app__navigation__link" activeClassName = "ui-app__navigation__link--active" to = "/post" exact = { true } > Post < Switch > < Route path = " / " exact = { true } render = { ( ) => < Counter name = " Monica Geller " /> } /> < Route path = " /post " exact = { true } component = { Post } /> ) } }

    // src/index.dev.js import React from 'react' import ReactDOM from 'react-dom' import { BrowserRouter } from 'react-router-dom' // import App components import { App } from './components/app' // compile App component in `#app` HTML element ReactDOM . render ( < BrowserRouter > < App /> , document . getElementById ( 'app' ) )

    // src/index.prod.js import React from 'react' import ReactDOM from 'react-dom' import { BrowserRouter } from 'react-router-dom' // import App components import { App } from './components/app' // compile App component in `#app` HTML element ReactDOM . hydrate ( < BrowserRouter > < App /> , document . getElementById ( 'app' ) )

    개발 서버를 다시 실행하면 URL 기본 '/' 경로와 일치하므로 Counter 컴포넌트가 렌더링 되는 것을 볼 수 있다. URL '/post'로 이동 시 Post 컴포넌트가 마운트 된다.

    클라이언트에서 BrowserRouter 가 URL 변경을 수신 대기하고 URL 경로에 맞게 각 컴포넌트를 렌더링한다.

    클라이언트에서 BrowserRouter 가 수행하던 역할을 서버에서 제공하도록 해야 한다. 이는 location prop 을 전달 받는 StaticRouter 컴포넌트를 사용해야 한다. Express 라우터 핸들러에서 받은 req.originalUrl 값을 location prop 값으로 사용할 수 있다.

    // server/express.js const express = require ( 'express' ) const fs = require ( 'fs' ) const path = require ( 'path' ) const React = require ( 'react' ) const ReactDOMServer = require ( 'react-dom/server' ) const { StaticRouter } = require ( 'react-router-dom' ) // create express application const app = express ( ) // import App component const { App } = require ( '../src/components/app' ) // serve static assets app . get ( / \. ( js | css | map | ico ) $ / , express . static ( path . resolve ( __dirname , '../dist' ) ) ) // for any other requests, send `index.html` as a response app . use ( '*' , ( req , res ) => { // read `index.html` file let indexHTML = fs . readFileSync ( path . resolve ( __dirname , '../dist/index.html' ) , { encoding : 'utf8' , } ) // get HTML string from the `App` component let appHTML = ReactDOMServer . renderToString ( < StaticRouter location = { req . originalUrl } > < App / > < / StaticRouter > ) // populate `#app` element with `appHTML` indexHTML = indexHTML . replace ( '

    ' , `

    ${ appHTML }

    ` ) // set header and status res . contentType ( 'text/html' ) res . status ( 200 ) return res . send ( indexHTML ) } ) // run express server on port 9000 app . listen ( '9000' , ( ) => { console . log ( 'Express server started at http://localhost:9000' ) } )

    이제 브라우저가 http://localhost:9000 및 http://localhost:9000/post 를 요청하면 URL에서 경로를 추출하여 StaticRouter에서 렌더링할 컴포넌트를 제어한다.

    이후에 브라우저에서 링크를 클릭하여 URL 경로 변경 시 React 앱은 HTML을 가져오기 위해 서버에 새 요청을 보내지 않고 클라이언트에서 처리하게 된다.

    Handling Data Fetch

    예시에서 Post 컴포넌트에는 하드 코딩된 제목과 설명이 있지만 API를 통해서 데이터를 가져오려고 한다. jsonplaceholer.com 에서 샘플 JSON 데이터를 가져올 것이다. Post 컴포넌트 내부에서 fetch 를 axios 를 사용하여 구현해보자.

    $ npm install -S axios

    Post 컴포넌트에서 fetchData 정적 메소드를 만들어 데이터를 가져오자. 이는 React 컴포넌트의 componentDidMount 에서 호출하여 데이터를 가져와 상태를 업데이트하고 렌더링하게 된다.

    // src\components\post\post.component.jsx import React from 'react' import axios from 'axios' export class Post extends React . Component { constructor ( ) { console . log ( 'Post.constructor()' ) super ( ) // component state this . state = { isLoading : true , title : '' , description : '' , } } // fetch data static fetchData ( ) { console . log ( 'Post.fetchData()' ) return axios . get ( 'https://jsonplaceholder.typicode.com/posts/3' ) . then ( ( response ) => { return { title : response . data . title , body : response . data . body , } } ) } // when component mounts, fetch data componentDidMount ( ) { console . log ( 'Post.componentDidMount()' ) Post . fetchData ( ) . then ( ( data ) => { this . setState ( { isLoading : false , title : data . title , description : data . body , } ) } ) } render ( ) { console . log ( 'Post.render()' ) return ( < div className = " ui-post " > < p className = " ui-post__title " > Post Widget { this . state . isLoading ? ( 'loading...' ) : ( < div className = " ui-post__body " > < p className = " ui-post__body__title " > { this . state . title } < p className = " ui-post__body__description " > { this . state . description } ) } ) } }

    위의 동작은 서버에서는 작동하지 않는다. renderToString() 메소더는 componentDidMount() 를 포함한 생명주기에 해당하는 메소드를 실행시키지 않는다. 실행되는 React 컴포넌트의 메소드는 생성자와 랜더이다.

    생성자 또는 렌더링에서 fetchData 메소드를 호출하려는 경우 API 요청의 비동기 특성으로 작동하지 않는다. express.js 서버에서 HTML을 반환하기 전에 컴포넌트에 대한 데이터를 수동으로 가져와서 Post 컴포넌트에 전달해야 한다.

    첫 번째 문재는 Post 컴포넌트가 App 컴포넌트 안에 포함되어있어 쉽게 접근할 수 없다는 것이다. 두 번째는 Post 컴포넌트 경로에 대한 데이터를 어떻게 가져올 것인가에 대한 문제이다.

    먼저 StaticRouter 에 의해 렌더링 된 컴포넌트를 식별하여 해당 컴포넌트에 대한 데이터를 가져와야 한다. 그런 다음 가져온 데이터를 라우터가 렌더링한 컴포넌트에 전달하고 서버에서 데이터를 제공한 경우 클라이언트에서 데이터를 다시 가져오는 것을 피해야 한다.

    // server\express.js const express = require ( 'express' ) const fs = require ( 'fs' ) const path = require ( 'path' ) const React = require ( 'react' ) const ReactDOMServer = require ( 'react-dom/server' ) const { StaticRouter , matchPath } = require ( 'react-router-dom' ) // create express application const app = express ( ) // import App component const { App } = require ( '../src/components/app' ) // import routes const routes = require ( './routes' ) // serve static assets app . get ( / \. ( js | css | map | ico ) $ / , express . static ( path . resolve ( __dirname , '../dist' ) ) ) // for any other requests, send `index.html` as a response app . use ( '*' , async ( req , res ) => { // get matched route const matchRoute = routes . find ( ( route ) => matchPath ( req . originalUrl , route ) ) // fetch data of the matched component let componentData = null if ( typeof matchRoute . component . fetchData === 'function' ) { componentData = await matchRoute . component . fetchData ( ) } // read `index.html` file let indexHTML = fs . readFileSync ( path . resolve ( __dirname , '../dist/index.html' ) , { encoding : 'utf8' , } ) // get HTML string from the `App` component let appHTML = ReactDOMServer . renderToString ( < StaticRouter location = { req . originalUrl } context = { componentData } > < App / > < / StaticRouter > ) // populate `#app` element with `appHTML` indexHTML = indexHTML . replace ( '

    ' , `

    ${ appHTML }

    ` ) // set value of `initial_state` global variable indexHTML = indexHTML . replace ( 'var initial_state = null;' , ` var initial_state = ${ JSON . stringify ( componentData ) } ; ` ) // set header and status res . contentType ( 'text/html' ) res . status ( 200 ) return res . send ( indexHTML ) } ) // run express server on port 9000 app . listen ( '9000' , ( ) => { console . log ( 'Express server started at http://localhost:9000' ) } )

    // server/routes.js const { Counter } = require ( '../src/components/counter' ) const { Post } = require ( '../src/components/post' ) module . exports = [ { path : '/' , exact : true , component : Counter , } , { path : '/post' , exact : true , component : Post , } , ]

    // src\components\post\post.component.jsx import React from 'react' import axios from 'axios' export class Post extends React . Component { constructor ( props ) { console . log ( 'Post.constructor()' ) super ( ) // component state if ( props . staticContext ) { this . state = { isLoading : false , title : props . staticContext . title , description : props . staticContext . body , } } else if ( window . initial_state ) { this . state = { isLoading : false , title : window . initial_state . title , description : window . initial_state . body , } } else { this . state = { isLoading : true , title : '' , description : '' , } } } // fetch data static fetchData ( ) { console . log ( 'Post.fetchData()' ) return axios . get ( 'https://jsonplaceholder.typicode.com/posts/3' ) . then ( ( response ) => { return { title : response . data . title , body : response . data . body , } } ) } // when component mounts, fetch data componentDidMount ( ) { if ( this . state . isLoading ) { console . log ( 'Post.componentDidMount()' ) Post . fetchData ( ) . then ( ( data ) => { this . setState ( { isLoading : false , title : data . title , description : data . body , } ) } ) } } render ( ) { console . log ( 'Post.render()' ) return ( < div className = " ui-post " > < p className = " ui-post__title " > Post Widget { this . state . isLoading ? ( 'loading...' ) : ( < div className = " ui-post__body " > < p className = " ui-post__body__title " > { this . state . title } < p className = " ui-post__body__description " > { this . state . description } ) } ) } }

    < html lang = " en " > < head > < meta charset = " UTF-8 " /> < meta name = " viewport " content = " width=device-width, initial-scale=1.0 " /> < meta http-equiv = " X-UA-Compatible " content = " ie=edge " /> < title > React Boilerplate / Webpack 4 / Babel 7 < meta name = " description " content = " React boilerplate with Webpack 4 and Babel 7 " /> < link rel = " icon " href = " /assets/favicon.ico " /> < script > var initial_state = null < body style = " background-color : #eee ; " > < div id = " app " >

    server/routes.js 는 App 컴포넌트 내부에서 렌더링 될 컴포넌트를 내보낸다. 경로에 일치하는 렌더링 컴포넌트에 접근할 수 있게 하며 정적 메소드인 fetchData 를 통해 데이터를 가져올 수 있다.

    StaticRouter 컴포넌트의 컨텍스트 prop을 사용하여 렌더링 된 컴포넌트 내부에 componentData 값을 전달한다. staticContext 로 전달되며 StaticRouter 에 의해 전달되었으므로 서버에만 존재한다.

    이전에 말했던 것처럼 componentDidMount 는 서버에서 실행되지 않으므로 서버에서 중복하여 데이터를 가져오는 것에 대해 걱정할 필요가 없다. 하지만 React.DOM.hydrate 가 호출되며 브라우저에서 실행이 된다.

    componentDidMount 가 hydrate 상태에서 호출되는 이유는 컴포넌트가 서버에서 생성된 HTML을 수정하려면 componentDidMount에서 setState 를 호출해야 하기 때문이다. 이 프로세스는 2단계 렌더링이라고 하며 이것은 해당 hydrate 링크를 참고하자.

    서버에서 데이터를 이미 가져왔을 때 데이터를 가져오는 것을 방지하기 위해서는 Post 컴포넌트에 데이터를 전달해야 한다. 이 방법으로는 index.html 에 전역 변수를 설정하고 componentData 로 해당 변수를 업데이트하는 것이다.

    이제 http://localhost:9000/post 경로를 방문하면 서버는 먼저 컴포넌트의 fetchData 메소드를 호출하여 컴포넌트 데이터를 가져온 다음 컨텍스트를 사용하여 컴포넌트에 데이터를 전달한다. StaticRouter 의 prop을 사용하여 컴포넌트가 데이터에 접근할 수 있도록 initial_data 를 전역 변수에 설정한다.

    지금까지 설명된 내용들은 아주 일부분이며 서버 측 렌더링은 훨씬 더 복잡해질 수 있다. 다음 GitHub 레파지토리에서 해당 문서에서 빌드한 예제와 전체 샘플 프로젝트를 확인할 수 있다.

    In Conclusion

    위 글을 번역하며 React 로 SSR 을 해보는 것을 간단하게 알 수 있었다. react 18 이 되며 모든 데이터를 가져와 완성된 HTML을 한 번에 내려주는 것이 아닌 부분적으로 hydrate를 수행하는 streaming HTML이라고 하는 선택적으로 hydration을 지원한다. 이러한 지원을 보며 Node 서버를 사용하여 React 로 SSR 을 처리하는 부분을 조금 더 공부해야겠다고 생각하게 되었다. 현재는 서비스에 우선순위가 높지 않고 Node 서버 관리 등의 추가적인 리소스 및 관리 포인트가 우려되어 반영할 것 같지는 않은 상태이다.

    이미 서버 쪽으로도 여러 기술이 나왔지만 사실상 포커스를 두어 공부를 하진 않았던 것 같다. 이제는 많은 곳에서 사용하는 Next.js 만 보더라도 손쉽게 SSR 을 할 수 있도록 도와주지만, 위와 같이 React 만으로 SSR 에 대한 동작이 어떻게 이루어지는지 알아보고 파악해보고 사용해 보는 것도 좋을 것 같다는 생각이 들었다.

    [Ref]:

    리액트를 다루는 기술 [개정판]: 20.1 서버 사이드 렌더링의 이해

    20 .1 서버 사이드 렌더링의 이해

    서버 사이드 렌더링은 UI를 서버에서 렌더링하는 것을 의미합니다. 앞에서 만든 리액트 프로젝트는 기본적으로 클라이언트 사이드 렌더링을 하고 있습니다. 클라이언트 사이드 렌더링은 UI 렌더링을 브라우저에서 모두 처리하는 것이죠. 즉, 자바스크립트를 실행해야 우리가 만든 화면이 사용자에게 보입니다.

    한번 CRA로 프로젝트를 생성하고 개발 서버를 실행해 보세요. 그리고 크롬 개발자 도구의 Network 탭을 열고 새로고침을 해 보세요.

    $ yarn create react-app ssr-recipe $ cd ssr-recipe $ yarn start

    ▲ 그림 20-1 비어 있는 root 엘리먼트

    맨 위에 있는 localhost를 선택하고 Response를 보면 root 엘리먼트가 비어 있는 것을 확인할 수 있습니다. 즉, 이 페이지는 처음에 빈 페이지라는 뜻이죠. 그 이후에 자바스크립트가 실행되고 리액트 컴포넌트가 렌더링되면서 우리에게 보이는 것입니다.

    서버 사이드 렌더링을 구현하면 사용자가 웹 서비스에 방문했을 때 서버 쪽에서 초기 렌더링을 대신해 줍니다. 그리고 사용자가 html을 전달받을 때 그 내부에 렌더링된 결과물이 보입니다.

    리액트 서버사이드 렌더링과 컴포넌트

    next + react 로 서버사이드 렌더링 환경을 구축하면서 개발을 하고 있었는데 두 가지 문제에 부딪혔었다.

    1. window is not defined , SSR 환경에서의 컴포넌트

    먼저 원래 코드를 보자.

    import Calendar from '@toast-ui/react-calendar' export default function Index ( ) { return ( < Calendar view = "month" month = { { narrowWeekend : true , } } onBeforeCreateSchedule = { ( e ) => { setOpenCreatePopup ( true ) setSelectedDate ( e . start . toDate ( ) ) } } onClickSchedule = { ( e ) => { console . log ( e ) } } scheduleView calendars = { calendars } schedules = { schedules } / > ) }

    Server Error ReferenceError: window is not defined This error happened while generating the page. Any console logs will be displayed in the terminal window. Call Stack Object. < anonymous > file:/// .. ./node_modules/tui-calendar/dist/tui-calendar.js ( 16 :4 )

    ( function webpackUniversalModuleDefinition ( root , factory ) { if ( typeof exports === 'object' && typeof module === 'object' ) module . exports = factory ( require ( "tui-code-snippet" ) , require ( "tui-date-picker" ) ) ; else if ( typeof define === 'function' && define . amd ) define ( [ "tui-code-snippet" , "tui-date-picker" ] , factory ) ; else if ( typeof exports === 'object' ) exports [ "Calendar" ] = factory ( require ( "tui-code-snippet" ) , require ( "tui-date-picker" ) ) ; else root [ "tui" ] = root [ "tui" ] || { } , root [ "tui" ] [ "Calendar" ] = factory ( ( root [ "tui" ] && root [ "tui" ] [ "util" ] ) , ( root [ "tui" ] && root [ "tui" ] [ "DatePicker" ] ) ) ; } ) ( window , function ( __WEBPACK_EXTERNAL_MODULE_tui_code_snippet__ , __WEBPACK_EXTERNAL_MODULE_tui_date_picker__ ) // 여기에서 에러가 난다.

    해당 컴포넌트는 최초 시작시에 window 가 필요한데, 서버사이드 렌더링 시에는 window 가 없는 환경이기 때문에 에러가 난다.

    아래 코드를 넣고, 최초 페이지 접근시에 새로고침을 하면 이 모듈이 실행되는 환경이 node 임을 알 수 있다.

    console . log ( 'node >> ' , globalThis === global ) // true

    결론적으로 이 컴포넌트는 서버사이드 렌더링을 지원하지 않고 있으며, 이를 해결하기 위해서는 window 가 있는 브라우저 환경에서만 import 해서 사용해야 한다. 이를 nextjs에서 처리하기 위해서는 아래와 같이 하면 된다.

    // dynamic 만으로는 부족하다. 꼭 ssr을 꺼야 한다. import dynamic from 'next/dynamic' const Calendar = dynamic ( ( ) => import ( '@toast-ui/react-calendar' ) , { ssr : false , } )

    2. Next SSR 환경에서의 ref

    export default function Index ( ) { const cal = useRef ( ) useEffect ( ( ) => { console . log ( cal . current ) } , [ cal ] ) return < Calendar ref = { cal } / > }

    위의 log 는 아래와 같이 찍힌다.

    { retry: ƒ } retry: ƒ ( ) arguments: ( .. . ) caller: ( .. . ) length: 0name: "bound retry" __proto__: ƒ ( ) [ [ TargetFunction ] ] : ƒ retry ( ) [ [ BoundThis ] ] : LoadableSubscription [ [ BoundArgs ] ] : Array ( 0 ) __proto__: Object

    useEffect 는 SSR에서 절대로 실행되지 않는다. 이를 해결하기 위해 useServerEffect라고 불리우는(?) 해괴한 effect가 있지만 , 굳이 그럴필요 없이 next의 getServerSideProps 를 사용하면 된다.

    왜 useRef 는 정상적으로 동작하지 않는 것일까?

    useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component. ... This works because useRef() creates a plain JavaScript object. The only difference between useRef() and creating a {current: ...} object yourself is that useRef will give you the same ref object on every render.

    https://reactjs.org/docs/hooks-reference.html#useref

    useRef 는 순수한 자바스크립트 객체이며, 컴포넌트가 아무리 렌더링이 된다고 해도 같은 ref 객체를 반환한다. 그런데 현재 ref.current 에는 retry 만 존재한다. 이것은 무엇일까?

    https://github.com/vercel/next.js/blob/f06c58911515d980e25c33874c5f18ade5ac99df/packages/next/next-server/lib/loadable.js#L219-L260

    https://github.com/vercel/next.js/blob/f06c58911515d980e25c33874c5f18ade5ac99df/packages/next/next-server/lib/loadable.js#L161-L173

    위 두 코드에 정답이 나와있다. useImperativeHandle 를 통해서 ref 를 노출하고 있기 때문에, current에는 현재 세팅되어 있는 retry 만 보이고 있었던 것이다. useImperativeHandle 는 forwardRef 와 사용해야 한다.

    useImperativeHandle customizes the instance value that is exposed to parent components when using ref . As always, imperative code using refs should be avoided in most cases. useImperativeHandle should be used with forwardRef :

    https://reactjs.org/docs/react-api.html#reactforwardref

    forwardRef 는 전달 받은 ref 속성을 하부트리의 다른 컴포넌트로 전달 할 수 있는 리액트 컴포넌트를 생성한다.

    // #components/TuiCalendarWrapper import React from 'react' import Calendar from '@toast-ui/react-calendar' export default ( props ) => ( // 3. 넘겨받은 `forwardedRef`를 진짜 컴포넌트에 넘긴다. < Calendar { ... props } ref = { props . forwardedRef } / > )

    const TuiCalendar = dynamic ( ( ) => import ( '#components/TuiCalendarWrapper' ) , { ssr : false , } ) // 2. forwardRef를 통해서 전달받은 ref를 하위 컴포넌트에 보낸다. const CalendarWithForwardedRef = React . forwardRef ( ( props , ref ) => ( < TuiCalendar { ... props } forwardedRef = { ref } / > ) ) export default function Index ( ) { const ref = useRef ( ) // 1. ref를 넘겨준다. return < CalendarWithForwardedRef ref = { ref } / > }

    Ref를 포워딩 하는 방법은 여기에 더 자세히 나와있다.

    https://reactjs.org/docs/forwarding-refs.html

    키워드에 대한 정보 리액트 서버사이드 렌더링

    다음은 Bing에서 리액트 서버사이드 렌더링 주제에 대한 검색 결과입니다. 필요한 경우 더 읽을 수 있습니다.

    이 기사는 인터넷의 다양한 출처에서 편집되었습니다. 이 기사가 유용했기를 바랍니다. 이 기사가 유용하다고 생각되면 공유하십시오. 매우 감사합니다!

    사람들이 주제에 대해 자주 검색하는 키워드 서버사이드 렌더링 (개발자라면 상식으로 알고 있어야 하는 개념 정리 ⭐️)

    • 개발
    • 웹개발
    • 프론트엔드
    • 백엔드
    • 코딩
    • 프로그래밍
    • 서버사이드 렌더링
    • 클라이언트 사이드 렌더링
    • 사이드 렌더링
    • 갯츠비
    • next.js
    • gatsby
    • server side rendering
    • client side rendering
    • static site generation
    • csr
    • ssr
    • ssg
    • spa
    • 풀스택
    • yt:cc=on

    서버사이드 #렌더링 #(개발자라면 #상식으로 #알고 #있어야 #하는 #개념 #정리 #⭐️)


    YouTube에서 리액트 서버사이드 렌더링 주제의 다른 동영상 보기

    주제에 대한 기사를 시청해 주셔서 감사합니다 서버사이드 렌더링 (개발자라면 상식으로 알고 있어야 하는 개념 정리 ⭐️) | 리액트 서버사이드 렌더링, 이 기사가 유용하다고 생각되면 공유하십시오, 매우 감사합니다.

    See also  난 할 수 있어 | 3 2 넌할수있어라고 말해주세요 2794 좋은 평가 이 답변

    Leave a Reply

    Your email address will not be published. Required fields are marked *