모던 리액트 4장
모던 리액트 공부 기록
2024-01-23
싱글 페이지 애플리케이션
싱글 페이지 애플리케이션은 렌더링과 라우팅에 필요한 대부분의 기능을 브라우저의 자바스크립트에 의존하는 것을 말한다. 최초로 첫 페이지에서 데이터를 불러 온 후, 페이지 전환이 브라우저의 history.pushState나 history.replaceState로 이뤄지기 때문에 페이지를 불러온 후부터는 서버에서 HTML을 추가로 내려받지 않는다.
싱글 페이지 애플리케이션에서 전체 사이를 모두 볼 수 있지만, 실제 소스 보기로 HTML을 보면 내부에 아무 내용이 없다.
사이트 렌더링에 필요한 body내부의 내용은 자바스크립트 코드를 삽입한 이후 렌더링하기 떄문이다. 또 페이지 전환 시에도 새 HTML을 요청하는 것이 아니라 다음 페이지의 렌더링에 필요하나 정보만 가져온 후 body 내부의 DOM으르 추가,삭제,수정하는 방법으로 페이지가 전환된다.
이런 싱글 페이지 애플리케이션은 자바스크립트 리소스가 커지는 단점이 있지만, 한번 로딩된 이후 서버를 거쳐 다시 리소스를 가져올 일이 적어져서 사용자에게 부드러운 페이지 전환을 재공한다.
서버 사이드 렌더링
싱글 페이지 애플리케이션이 자바스크립트를 통해 하나의 페이지에서 렌더링을 수행한다면, 서버 사이드 렌더링은 최초 사용자에게 보여주는 페이지를 서버에서 렌더링해 빠르게 사용자에게 보여주는 방식을 의미한다. 싱글 페이지 애플리케이션에서 자바스크립트의 크기가 커지면 커질수록 웹 페이지가 느려지는 것을 방지하고자, 서버에서 페이지를 렌더링해 제공하는 서버사이드-렌더링이 다시 주목을 받고 있다.
싱글 페이지 애플리케이션과 서버에서 페이지를 빌드하는 서버 사이드 렌더링은 웹 페이지의 렌더링의 책임을 어디에 두냐이다. 싱글 페이지 애플리케이션은 사용자에게 제공되는 자바스크립트 번들에서 렌더링을 책임지지만, 서버 사이드 방식은 렌더링의 역할을 모두 서버에서 수행한다.
서버사이드 렌더링의 장점으로는 다음의 것들이 존재한다.
-
최초 페이지 진입이 비교적 빠르다. 사용자가 최초 페이지에 진입했을 때 페이지의 정보가 그려지는 시간 (FCP)가 더 빨라질 수 있다. 최초에 사용자가 보게 될 화면이 외부 API에 의존적이라면, 싱글 페이지 애플리케이션의 경우 페이지 진입 -> 자바스크립트 번들 다운 -> HTTP 요청 -> 렌더링 과정을 거친다.
그러나 이런 작업은 서버에서 더 빠르게 진행될 수 있다.(서버가 리소스를 확보한 상태라면)
-
검색 엔진과 SNS 공유 등의 메타데이터 가공이 쉽다.
-
누적 레이아웃 이동이 적다. 누적 레이아웃 이동은 사용자에게 페이지를 보여준 이후 뒤늦게 어떤 HTML 정보가 추가되거나 삭제되면서 화면이 덜컥거리는 것과 같은 부정적인 사용자 경험을 말한다. 싱글 페이지 애플리케이션에서는 페이지 콘텐츠가 API요청에 의존할 수 있다. 이때 API요청의 응답 속도가 제각각이면 이런 누적 레이아웃 이동이 발생할 수 있다. 이는 그러나 React18의 스트림으로 해결이 가능하다.
-
사용자의 디바이스 성능에 비교적 자유롭다.
다음으로 단점으로는 다음의 것들이 존재한다.
-
소스코드 작성시 항상 서버를 고려해야 한다. (window,sessionStorage 등등..)
-
적절한 서버가 구축되어 있어야 한다. 사용자의 요청을 받아 렌더링을 적절히 수행할 서버가 필요하고, 예기치 않은 장애 상황에 대응하는 전략도 필요하다. Next EC2-배포-PM2-활용한-무중단-배포
현대의 서버 사이드 렌더링은 기존의 서버 사이드 렌더링와 약간 다르다. 최초 웹 사이트 진입 시에는 서버 사이드 렌더링으로 서버에서 완성된 HTML을 제공받고,이후 라우팅에서는 서버에서 내려받은 자바스크립트를 바탕으로 마치 싱글 페이지 애플리케이션처럼 작동한다.
서버 사이드 렌더링을 위한 리액트의 API 알아보기
리액트는 브러우저 자바스크립트 환경에서 렌더링 할 수 있는 방법을 제공하지만, 동시에 애플리케이션을 서버에서 렌더링하는 API도 제공한다. 이 api는 Node환경에서만 실행할 수 있다.
- renderToString : 리액트 컴포넌트를 렌더링 해 HTML 문자열로 바꾸는 함수다.
이 result는 다음과 같이 변환된다.
useEffect훅이나 handleOnClick 등의 이벤트 핸들러는 결과물에서 제외된다. 웹페이지가 사용자와 인터렉션할 준비가 되기 위해서는 별도의 자바스크립트 코드를 모두 다운받고, 파싱하고 실행해야 한다. data-reactroot는 리액트 컴포넌트의 루트가 무엇인지 식별하는 역할을 한다.
이 속성은 이후 자바스크립트를 실행하기 위한 hydrate함수에서 루트를 식별하는 기준이 된다.
-
renderToStaticMarkUp : 앞서 renderToString과 유사하나 data-reactroot등의 리액트에서만 쓰는 추가적인 DOM 속성을 만들지 않는다. 이 함수는 이벤트 리스너가 필요없는 완전한 정적의 HTML을 만들 때 유용하다.
-
renderToNodeStream : renderToString과 결과물이 동일하지만 크게 2가지의 차이점이 있다. 앞서 함수와 달리 브라우저에서 사용하는 것이 금지된다. renderToNodeStream은 결과물이 Node의 ReadableStream이다. (utf-8로 인코딩된 바이트 스트림!)
왜 그럼 이 함수가 필요할까 ?
유튜브 영상을 보기 위해 전체 영상을 다운받을 때까지 기다리지 않는다. 사용자가 볼 수 있는 조금이라도 다운로드되면 그 부분을 먼저 보여준다. 스트림은 큰 데이터를 다룰 시 청크단위로 분할해 조금씩 가져오는 방식을 말한다.
만약 renderToString의 결과물이 매우 크다면 어떨까? 크기가 큰 문자열을 한번에 올려두고 응답을 수행하면 Node 서버에 큰 부담이 될 수 있다.
이렇게 하면 응답으로 오는 HTML이 여러 청크로 분리되어 내려오는 것을 볼 수 있다.
- hydrate : 이 함수는 renderToString이나 renderToNodeStream으로 생성된 HTML 콘텐츠에 자바스크립트 이벤트 핸들러나 이벤트를 붙이는 역할을 한다. 앞서 renderToString은 HTML 렌더링된 결과물을 사용자에게 보여줄 수 있지만, 사용자와 페이지에서 상호작용하는 것은 불가능하다.이렇게 hydrate는 정적으로 생성된 HTML에 이벤트와 핸들러를 붙여 웹페이지 결과물을 만든다.
render와 달리 이미 렌더링된 HTML이 있다는 가정하에 작업을 하고, 이 렌더링된 HTML을 기준으로 이벤트를 붙이는 작업만 진행된다.
Next알아보기
eslint-config-next : Next기반 프로젝트에서 쓰는 Eslint설정이다. 이 설정은 정말 많은 규칙을 포함한다.
이 린트를 이용해 커스텀 규칙도 만들 수 있다.
기본적으로 Next는 린트를 pages하위의 모든 파일, app 하위의 모든 파일, components , lib,src폴더의 모든 하위 파일에 적용한다. 그러나 이 기본 값을 재설정해, 필요한 폴더의 하위에서만 린트를 동작하게 바꿀 수도 있다.
next/core-web-vitals 규칙은 만약 Core-Web-Vitals의 규칙에 위배되는 부분이 있다면 린트에서 오류를 알려주는 규칙이다.(이 core-web-vitals는 더 파보면 좋을듯..)
next lint를 lint-staged와 같이 사용하고자 한다면 .lintstagedrc.js파일을 루트에 만들면 된다!
next config 파일은 next 프로젝트의 환경 설정을 담당하고 여러 옵션을 통해 프로젝트의 설정을 바꿀 수 있다.
- reactStrictMode : 리액트의 엄격모드와 관련한 옵션이다. 기본적으로 켜주는 것이 좋다!
- swcMinify : 번들링과 컴파일을 더 빠르게 해주는 역할이다(rust로 만들어진).. 소스 최적화 작업을 할 것인지 여부를 나타낸다.
_app.tsx 그리고 내부의 default export로 내보낸 함수는 모든 전체 페이지의 시작점이다. 즉 프로젝트 전체에서 공통적으로 사용해야 할 것들을 여기서 설정할 수 있다.
- 에러 바운더리와 같은 전역 에러 처리
- CSS의 Reset
- 모든 페이지의 공통적으로 제공되는 데이터
최초에는 서버 사이드 렌더링을, 이후에는 클라이언트에서 _app.tsx의 렌더링이 실행된다.
Next 13의 app router에서는 전역의 layout.tsx가 이 _app.tsx을 대신한다.
_document.tsx은 애플리케이션의 HTML을 초기화 하는 곳이다.
-
HTML이나 Body에 DOM속성을 추가하고 싶을 때 이용한다. (13 버전에서는 전역 layout.tsx에서 Script를 쓰거나 dangerouslySetInnerHTML={{__html:``}}를 사용하면 된다)
-
document.tsx는 무조건 서버에서 실행되고 onClick과 같은 이벤트 핸들러의 등록은 불가능하다.
-
getServerSideProps,getStaticProps 등 서버에서 데이터를 조회하는 함수를 사용할 수 없다.
-
CSS-in-JS의 스타일을 서버에서 모아 전송할 수 있다.
pages/error.tsx는 클라이언트나 서버에서 발생하는 에러를 처리할 목적으로 만들어졌다. 전역에서 발생하는 에러를 적절히 처리할 때 사용할 수 있다.
(13버전에서는 error.tsx를 통해 에러를 처리할 수 있다.이 부분은 더 자세히 파볼 수 있을 것 같다.!!)
pages/404.tsx는 404페이지를 정의하는 파일이다. 원하는 404스타일의 페이지를 이곳에서 만들 수 있다.
pages/index.tsx의 default export로 내부낸 함수가 해당 페이지의 루트 컴포넌트가 된다.
-
pages/index.tsx : 웹 사이트의 루트이며 localhost:3000과 같은 루트 주소를 의미한다.
-
pages/hello.tsx : 파일명이 주소가 된다. 여기서는 /hello이며 localhost:3000/hello로 접근할 수 있다.
-
pages/hello/world.tsx : localhost:3000/hello/world로 접근이 가능하다.
-
pages/hello/[greeting].tsx : []의 의미는 어떤 문자도 올 수 있다는 것이다. 예를 들어 localhost:3000/hello/1, localhost:3000/hello/khj의 주소도 모두 유효하다. 그리고 이 때 저 greeting에는 1과 khj가 들어간다.
-
pages/hi/[...props].tsx : hi를 제외한 hi의 모든 하위의 주소가 온다. 예를 들어 localhost:3000/hi/hello, localhost:3000/hi/hello/world 등의 주소가 올 수 있다. 그리고 이 [...props]에 값이 들어간다.
서버 라우팅과 클라이언트 라우팅
Next의 최초 렌더링은 서버에서 실행된다.
콘솔 문구가 서버에서 기록되고 window가 undefined이기 때문에 server가 로그에 찍힐 것이다. next/link에서 제공하는 Link와 a태그는 어떻게 다를까?
a태그를 사용하면 모든 리소스를 다시 다운받지만, link컴포넌트를 사용하면 모든 리소스가 아닌, 이동한 페이지에서 필요한 리소스만 다운받는다.
마치 싱글 페이지 애플리케이션처럼 매끄럽게 이동하는 것을 볼 수 있다.
이렇게 페이지 사이의 관계를 라우팅으로 나타내면, 다음으로 어떻게 움직일지 정하는 것이 중요하다.
만약 /users 페이지에서 특정 유저의 정보를 조회하고자 /users/123 이런 링크로 넘어가면 해당 링크에 대한 새로운 HTML,JS을 서버에 요구한다.
Next는 기본적으로 요청에 따라 Static HTML,SSR을 할 경우 완성된 HTML을 던져주고, 브라우저가 자바스크립트를 다 다운받으면 흔히 리액트가 돌아가는 것처럼 DOM을 구성해 보여준다.(이것을 Hydration이라 한다.)
실제 Link컴포넌트를 써서 링크를 눌러서 들어가면 매번 HTML 파일이 오지 않는다는 것을 볼 수 있다. 들어갈 가능성이 있는 페이지는 "미리 가져오고", "매번 HTML을 받지 않는다."
이것을 Prefetching이라 부른다.
next/link는 백그라운드에서 페이지(주소에 의해 표시됨)를 미리 가져오게 된다. 이는 클라이언트 측 내비게이션의 성능을 향상시키는데 유용합니다. 뷰포트에 있는 모든 (초기 또는 스크롤을 통해)는 미리 로드됩니다.
뷰포트에 있는 모든 Link가 미리 로드된다고 한다. 실제 뷰포트에 없다가 Link가 뷰포트에 생기면 미리 로드하는 것을 아래의 영상에서 볼 수 있다.
이 때 Server-Side-Rendering의 페이지는 자바스크립트를 다시 로드하고 Static-Site-Generation의 페이지는 JSON을 다시 돌려준다.
여러 옵션으로는 다음의 것들이 있다.
-
href: 이동하고자 하는 페이지의 URL을 지정합니다.
-
as: 브라우저에서 URL의 경로를 변경할 때 사용되는 가상 경로(virtual path)를 지정합니다.
-
replace: 이동할 때 페이지를 새로고침하지 않고 기존의 페이지를 대체하는 옵션입니다.
-
scroll: 새로운 페이지로 이동할 때 스크롤 위치를 조절하는 옵션입니다.
-
shallow: 브라우저의 히스토리 스택에서 새로운 항목을 생성하지 않고 이전 페이지로 돌아가는 옵션입니다.
-
passHref: a 태그에 href 속성을 추가할 지 여부를 결정하는 옵션입니다.
-
prefetch: 미리 페이지를 불러와 캐시해 두는 옵션입니다.
getServerSideProps가 있는 빌드는 빌드 결과에 서버 사이드 런타임 체크가 되어 있다. 만약 getServerSideProps가 없으면 서버에서 실행되지 않는 페이지로 간주하고 빌드 시점에 미리 트리쉐이킹을 한다. (그러나 13버전에서 getServerSideProps가 아예 사라졌다)
Data Fetching
Next에서 데이터를 불러오기 위한 여러 전략이 있는데, 이를 Data-Fetching이라고 부른다. ~12버전에서 사용할 수 있는 함수들이 여러가지 있고 13버전에서는 아예 fetch함수로 통일이 되어버렸는데 하나씩 살펴보자.
- getStaticProps와 getStaticPaths
이 두 함수는 어떤 페이지를 블로그나 게시판 같이 정적으로 결정된 페이지를 보여주고자 할 때 사용하는 함수이다. 예를 들어 /pages/post/[id]의 페이지가 있고 해당 페이지에 다음과 같은 함수를 사용했다고 가정하자.
getStaticPaths는 pages/post/[id]가 접근 가능한 주소를 정의하는 함수다. 즉 이 페이지는 /post/1과 /post/2만 접근 가능함을 의미하고, post/3에서는 404페이지를 반환한다.
getStaticProps는 앞에서 정의한 페이지를 기준으로 해당 페이지로 요청이 들어올 때 제공할 props를 반환한다. 이 예제는 id가 1,2,로 제한되어 있기 때문에 fetchPost(1) fetchPost(2)의 응답 결과를 props의 post로 전달한다.
getServerSideProps는 서버에서 실행되는 함수이며, 해당 함수가 존재한다면 무조건 페이지 진입 전에 이 함수를 실행한다. 이 함수는 응답값에 따라 페이지의 루트 컴포넌트에 props를 반환하기도, 다른 페이지로 리다이렉트를 할 수 있다.
context.query.id를 사용하게 된다면 /post/[id]와 같은 경로에 있는 id값에 접근할 수 있다. getServerSideProps의 props로 내릴 수 있는 값은 JSON으로 제공가능한 값으로 제한된다.
먼저 리액트의 서버 사이드 렌더링을 하는 과정에 대해 알아보자!
- 서버에서 fetch 등으로 렌더링에 필요한 정보를 가져온다.
- 1번의 정보를 바탕으로 HTML을 완성한다.
- 2번의 정보를 클라이언트(브라우저에) 전달한다.
- 3번의 정보를 바탕으로 Hydrate작업을한다.
- 4번의 작업인 Hydrate로 만든 컴포넌트 트리와 서버의 HTML이 다르면 불일치 에러를 뱉는다.
next config
-
basePath : 만약 "docs"와 같이 문자열을 추가하면 localhost:3000/docs에 서비스가 시작된다. 클라이언트 렌더링을 트리거하는 모든 주소에 basePath가 붙는다.
-
assetPrefix : next에서 빌드한 결과물을 다른 CDN등에 업로드하고자 할때 주소를 명시한다.