티스토리 뷰
문제상황
Next.js 14 에서 프로젝트를 생성하면 기본적으로 /app 디렉토리 내의 모든 컴포넌트들은 서버 컴포넌트이다. 따라서 클라이언트 컴포넌트로 사용하려면 상단에 use client 를 붙여주어야 한다.
그런데 작업 중 깜빡하고 useState를 사용하는 컴포넌트에 use client를 붙이지 않았는데 앱이 에러 없이 잘 동작하는 것을 보게 되었다.
일시적인 현상인가 싶어 새로고침을 해보았는데 여전히 앱이 잘 작동하길래 당황했다. use client 매번 달아주는 거 귀찮았는데 살짝 좋기도 하고..
클라이언트 컴포넌트에는 use client를 무조건 붙여야 한다고 알고 있었는데 use client가 없어도 잘 작동하는 이유가 뭘까?
React가 UI를 표현하는 방법
이 현상을 이해하기 위해 먼저 React가 UI를 표현하는 방법인 모듈 의존성 트리와 렌더 트리에 대한 개념을 알아야 한다.
HTML은 DOM 트리로, CSS는 CSSOM 트리로 표현되듯이 react 역시 컴포넌트를 트리 형태로 관리한다.
이 트리는 부모 컴포넌트가 자식 컴포넌트를 포함하는 계층 구조 형태이고 UI의 복잡성을 체계적으로 다룰 수 있도록 도와준다.
1. 렌더 트리 (Real DOM Tree)
렌더 트리는 React 컴포넌트 간의 중첩 관계를 나타낸다. DOM을 렌더링 할 때 생성되며 컴포넌트만 캡슐화하기 때문에 리액트 앱에서 처리한다.
React 공식문서에서 가져온 명언을 렌더링하는 앱을 예시로 살펴보자.
import FancyText from './FancyText';
import InspirationGenerator from './InspirationGenerator';
import Copyright from './Copyright';
export default function App() {
return (
<>
<FancyText title text="Get Inspired App" />
<InspirationGenerator>
<Copyright year={2004} />
</InspirationGenerator>
</>
);
}
위 코드의 트리 구조는 각 컴포넌트가 트리의 노드로 나타나고, 부모-자식 관계를 통해 계층적으로 연결된다.
2. 모듈 의존성 트리 (Module Dependency Tree)
의존성 트리는 React 앱의 모듈 의존성을 나타낸다.
애플리케이션을 배포하기 위해 필요한 코드를 번들로 묶는 빌드 도구에서 사용되기 때문에 React가 아니라 번들러 (Webpack, Vite 등) 에서 처리한다.
따라서 불러오는 모듈을 모두 포함하기 때문에 앱의 규모가 커지면 번들 크기 또한 증가하게 된다.
정리
- 렌더 트리는 컴포넌트의 렌더링 구조를 나타내고 UI가 실제로 어떻게 구성되는지를 보여준다.
- 모듈 의존성 트리는 모듈 간의 연결 관계를 나타내고 어떤 파일들이 함께 묶여서 번들링 되는지를 결정한다.
use client
use client는 렌더 트리가 아닌 모듈 의존성 트리를 기준으로 클라이언트 모듈의 하위 트리를 생성한다.
만약 InspirationGenerator.js에 use client 지시어를 선언했다면 InspirationGenerator.js과 이 파일이 의존하는(import 하는) 모든 하위 모듈들은 클라이언트 모듈이 된다.
앞서 클라이언트 컴포넌트로 동작해야 하는 컴포넌트에 use client를 붙이지 않았는데 제대로 동작했던 이유가 바로 이러한 이유 때문이다. 부모 모듈에서 use client를 정의해주면 그 안으로 import된 모든 모듈이 클라이언트 컴포넌트로 간주되기 때문이다.
"Error occurred prerendering page ~" 에러가 발생한 이유
나의 경우 앱을 빌드할 때 Error occurred prerendering page ~. File is not defined 에러가 발생했었다.
이 에러는 전체 페이지 동작 단계에서 서버에서 미리 HTML이 생성되는 단계인 Pre-rendering이 실패해서 나타나는 에러라고 한다.
Next.js 공식 문서에서는 Pre-rendering 중 에러가 발생하는 경우 에러 발생의 주된 원인으로 아래의 4가지를 꼽고 있다.
1. pages/ 디렉토리 내 잘못된 파일 구조나 페이지 파일이 아닌 파일들이 위치한 경우
2. 사전 렌더링 과정에서 사용할 수 없는 props가 채워질 것으로 예상하는 경우
3. 컴포넌트에서 적절한 확인 없이 브라우저 전용 API를 사용하는 경우
4. getStaticProps 또는 getStaticPaths에서 잘못된 구성을 사용하는 경우
앞서 발생한 에러는 3번에 해당했다. 따라서 use client를 붙여서 에러를 해결할 수 있었다.
Pre-rendering
에러의 원인이었던 Pre-rendring은 무엇일까?
Pre-rendring은 Next.js가 각 페이지에 대한 HTML을 생성하는 것을 말한다. Next.js 에서는 기본적으로 프리렌더링을 지원하는데, 서버에서 HTML을 미리 내려주기 때문에 SEO(Search Engine Optimization)에 뛰어나다는 장점이 있다.
Pre-rendering 방식은Static Rendering과 Dynamic Rendering이 존재하고, 두 방식은 언제 HTML을 생성하느냐에 차이가 있다.
1. Static Rendering은 빌드(next build) 타임에 HTML이 생성된다. 이 결과는 캐싱되어 매 요청마다 재사용된다. (page router에서의 SSG와 ISR에 상응한다)
2. Dynamic Rendering은 매 요청마다 HTML이 생성된다. (page router에서의 SSR에 상응한다)
성능상의 이유로 Next.js에서는 첫번째 방식을 추천한다. 이유는 추가적인 설정 없이도 CDN에 의해 캐싱되어 퍼포먼스 향상을 노릴 수 있기 때문이다.
프리렌더링의 개념을 정리하고보니 한 가지 의문이 생겼다.
클라이언트 컴포넌트는 서버에서 렌더링 되지 않을텐데 왜 Pre-rendering 단계에서 에러가 발생한 걸까?
해답은 공식문서에서 찾을 수 있었다. 아래는 전체 페이지가 로드되는 과정에 대한 설명이다.
To optimize the initial page load, Next.js will use React's APIs to render a static HTML preview on the server for both Client and Server Components. This means, when the user first visits your application, they will see the content of the page immediately, without having to wait for the client to download, parse, and execute the Client Component JavaScript bundle.
해석하자면 서버 컴포넌트 뿐 만 아니라 클라이언트 컴포넌트도 서버에서 정적 HTML로 먼저 렌더링된다. 따라서 사용자가 애플리케이션에 방문하면 클라이언트가 JavaScript 번들을 다운로드하고 구문 분석 및 실행할 때까지 기다릴 필요 없이 페이지의 내용이 바로 표시된다는 의미이다.
충격.. 클라이언트 컴포넌트도 서버 환경에서 렌더링 된다는 것을 처음 알았다..
페이지가 처음 로드될 때 일어나는 전체 과정을 통해 무슨 의미인지 알아보자.
1. 서버 측
- React는 서버 컴포넌트를 RSC(React Server Component) Payload 라는 데이터 형식으로 변환한다. 여기에는 클라이언트 컴포넌트에 대한 참조가 포함되어 있다.
- 이후 Next.js에서 RSC Payload와 클라이언트 컴포넌트 JavasScript 지침을 사용해 HTML이 생성된다.
2. 클라이언트 측
- 브라우저는 생성된 HTML을 표시한다.
- RSC Payload를 사용해서 클라이언트와 서버 트리를 조정하고 DOM을 업데이트 한다.
- JavaScript 지침을 사용해서 클라이언트 컴포넌트를 Hydration한다.
초기 로드 이후에는 CSR(Client Side Rendering) 방식으로 처리된다.
따라서 페이지 이동 시 새롭게 HTML이 요청되지 않고 JavaScript 번들이 준비될 때 RSC Payload를 사용하여 클라이언트와 서버 컴포넌트 트리를 조정하고 DOM을 업데이트한다.
(자세한 내용은 공식문서에..)
브라우저 API를 사용할 때 use client를 붙여야 하는 이유
위 내용을 토대로 생각해보자면 React Hooks(useState, useEffect)는 클라이언트에서 하이드레이션 된 후에 실행되니까 에러가 나지 않았던 것 이지만, 브라우저 API를 사용했을 때 에러가 났던 이유는(클라이언트 경계 내에 있더라도) 클라이언트 컴포넌트, 서버 컴포넌트 상관 없이 초기 HTML 생성 때 서버에서 한 번 렌더링 되기 때문이다.
그렇다면 use client의 의미는 '이 컴포넌트는 클라이언트 환경에서만 실행된다' 가 아니라 '이 컴포넌트는 클라이언트에서 하이드레이션 되어야한다' 가 아닐까..?
모든 서버 컴포넌트에 use client를 사용하면 SEO가 나빠질까?
클라이언트 컴포넌트가 서버에서 HTML로 변환이 된다면 SEO 성능에도 큰 문제가 없을 것 같은데, 궁금해서 관련 내용에 대해 찾아보았다.
https://github.com/vercel/next.js/discussions/67878
'use client' have an impact on SEO? · vercel next.js · Discussion #67878
Summary If we use 'use client' in all Server Components of next.js, will SEO become so poor that it is almost the same as React? Or will SEO not be affected? Additional information No response Exam...
github.com
Next.js 레포지토리의 Discussions 탭 에서 "모든 Next.js 서버 컴포넌트에 'use client'를 사용하면 SEO가 React와 거의 동일할 정도로 나빠질까요? 아니면 SEO에 영향이 없을까요?" 에 대한 질문이 올라왔고 그에 대한 답변이 달렸는데, 답변을 요약해보자면 모든 컴포넌트에 'use client'를 사용해도 SEO에 큰 영향을 주지 않는다고 한다.
클라이언트 컴포넌트도 어쨌건 빌드시에 서버에서 초기 HTML을 생성하는 방식으로 작동하기 때문이다.
따라서 SEO 엔진이 이 HTML을 볼 수 있기 때문에 문제가 없다는 이야기이다.
하지만 클라이언트 컴포넌트에 사용자 상호작용을 통해서만 렌더링되는 콘텐츠가 있다면(아래 코드처럼 h1을 클릭 한 경우에만 div 태그가 렌더링 되는 경우) 그 부분은 초기 HTML에 포함되지 않기 때문에 약간은 영향이 있을 수 있다고 한다.
마치며
에러를 해결하는 과정도 아니고, 우연히 use client를 붙이지 않아서 신기한 마음에 찾아본 내용들이 결과적으로 보면 되게 의미있는 내용이었던 것 같다.
정리하면서 RSC와 SSR에 대해 헷갈리는 부분도 있어서 이건 나중에 따로 찾아봐야겠다.
참고
https://www.joshwcomeau.com/react/server-components/
https://velog.io/@9rganizedchaos/React-Server-Components%EB%A5%BC-%EC%9D%B4%ED%95%B4%ED%95%B4%EB%B3%B4%EC%9E%90https://nextjs-ko.org/docs/app/building-your-application/rendering/client-components
https://ko.react.dev/learn/understanding-your-ui-as-a-tree#the-module-dependency-tree
https://nextjs-ko.org/docs/app/building-your-application/rendering/client-components
https://saengmotmi.netlify.app/react/what-is-rsc/
https://www.reddit.com/r/nextjs/comments/1c80rfp/if_using_use_client_in_all_components_why_use/?rdt=37887
'Client > Next.js' 카테고리의 다른 글
[Next.js] TanStack Query의 prefetch와 dehydrate로 ChunkLoadError 에러 해결하기 (4) | 2024.10.04 |
---|---|
[Next.js] Next.js 프로젝트를 Vercel에 배포하기: 커스텀 도메인 설정부터 Production 배포까지 (0) | 2024.09.22 |
[Next.js] Invalid JSON (trailing comma, dangling comma, terminal comma) (0) | 2024.06.07 |
[Next.js] 외부 이미지 사이즈 지정하기 (0) | 2024.06.05 |
[Next.js] 'next'은(는) 내부 또는 외부 명령, 실행할 수 있는 프로그램, 또는배치 파일이 아닙니다. (0) | 2024.06.04 |
- Total
- Today
- Yesterday
- 제어 컴포넌트
- javascript
- 동기
- Target
- html
- hydrationboundary
- CSS
- 비제어 컴포넌트
- 배열
- 유사배열객체
- tanstackquery
- 프론트엔드
- 코드잇 스프린트
- innerhtml
- currentTarget
- 코드잇스프린트
- 중급 프로젝트
- 취업까지달린다
- 스프린트프론트엔드6기
- GitHub
- rest parameter
- 비동기
- js
- map
- 리액트
- Next.js
- 객체
- react
- arguments
- Git
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |