27강 Context내 RSC 사용 관련 문의
34
投稿した質問数 1
강의 잘 듣고있습니다. 한 가지 궁금한 점이 있어 질문드립니다.
"App.tsx를 Provider로 감싸는 순간 그 아래 모든 컴포넌트가 서버 컴포넌트로서의 최적화 기회를 잃는다"는 부분이 잘 이해되지 않습니다.
제가 알기로는 Provider를 children prop으로 받는 composition pattern으로 작성하면, Provider 자체는 Client Component여도 그 안에 들어가는 children은 Server Component로 유지될 수 있는 것으로 알고 있습니다. Next.js App Router의 RootLayout에서 <Providers>{children}</Providers> 형태로 쓰는 패턴이 그 예시라고 알고 있고요.
이 경우에도 RSC 최적화 기회를 잃는다고 보시는 건지, 아니면 제가 놓치고 있는 다른 맥락이 있는지 궁금합니다.
回答 3
1
안녕하세요 Eden님! 남겨주신 질문을 보니 React Server Component, 즉 RSC의 아키텍처와 Composition 패턴에 대해 깊이 있게 이해하고 계시네요. 결론부터 말씀드리면 Eden님이 알고 계신 내용이 맞습니다. Client Component인 Provider로 감싸더라도 children prop을 통해 Composition 형태로 넘기면, 해당 children으로 전달된 컴포넌트들은 Server Component의 속성을 그대로 유지하며 렌더링 최적화 기회를 잃지 않습니다.
질문해주신 것처럼 실습 자료의 "정밀 아키텍처 분석"에 적힌 "App.tsx를 10개의 Provider로 감싸는 순간 그 아래의 모든 컴포넌트는 서버 컴포넌트로서의 최적화 기회를 잃게 됩니다"라는 설명은, Eden님의 예상대로 Composition Pattern을 적용하지 않고 내부에서 직접 import하는 일반적이고 잘못된 안티 패턴을 전제로 설명한 것이 맞습니다. 더불어 패턴을 올바르게 사용하더라도 피할 수 없는 Context API의 근본적인 한계와 Client Boundary의 연쇄적인 오염을 함께 경고하기 위한 맥락이었습니다. 이 부분이 실무에서도 자주 발생하는 아키텍처적 실수이므로 왜 그런 표현이 나왔는지 이면의 동작 원리와 실무 예제, 그리고 추가적인 응용 방안까지 상세히 풀어드리겠습니다.
RSC 아키텍처에서 Server Component를 Client Component로 만드는 조건은 단순히 물리적으로 내부에 위치하는가에 달려있는 것이 아니라, 어떻게 import 되고 렌더링 되는가에 달려 있습니다. 이 과정에서 가장 흔히 하는 실수는 Client Component 내부에서 Server Component를 직접 import하여 렌더링하는 경우입니다.
'use client';
import { ThemeContext } from './context';
import Header from './Header'; // Server Component로 의도했으나 Client로 강제 변환됨
import MainContent from './MainContent'; // 마찬가지로 Client 영역으로 편입됨
export default function GlobalProvider() {
return (
<ThemeContext.Provider value="dark">
<Header />
<MainContent />
</ThemeContext.Provider>
);
}
위의 예시 코드처럼 'use client'가 선언된 파일 내부에서 직접 import된 컴포넌트들은 트리 구조상 Client Boundary 안으로 끌려 들어가게 되어, 서버에서 렌더링될 기회를 잃고 전부 클라이언트 번들에 포함됩니다. 앞서 말씀드린 강의에서 경계했던, Composition Pattern을 적용하지 않은 가장 위험한 패턴이 바로 이것입니다. 반면에 Eden님이 말씀하신 대로 Next.js의 Root Layout에서 흔히 사용하는 children 패턴을 쓰면 이 문제를 우회할 수 있습니다.
// Providers.tsx
'use client';
export default function Providers({ children }: { children: React.ReactNode }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}
// layout.tsx (Server Component)
import Providers from './Providers';
import Header from './Header'; // Server Component 유지
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>
<Header />
{children} {/* Server Component 유지 */}
</Providers>
</body>
</html>
);
}
이 올바른 패턴이 동작하는 핵심 원리는 평가, 즉 Evaluation 시점의 차이에 있습니다. Header와 children은 Server Component인 layout.tsx에서 미리 평가되어 직렬화된 React 객체(RSC Payload) 형태로 Providers의 prop으로 전달됩니다. 클라이언트 입장에서는 컴포넌트 함수를 직접 실행하는 것이 아니라, 서버가 전달해 준 렌더링 결과물을 그대로 화면에 얹기만 하므로 RSC의 이점을 유지할 수 있는 것입니다.
이렇게 Composition 패턴을 쓰면 RSC가 유지됨에도 불구하고 Root 레벨을 Provider로 감싸는 것을 지양하라고 강조했던 더 깊은 이유가 있는데, 이는 바로 데이터 소비의 문제 때문입니다. RSC는 서버에서 실행되므로 브라우저의 API나 상태를 가질 수 없으며, 트리 하단 깊숙한 곳에 있는 Server Component가 Root에 정의된 Context 값을 읽기 위해 useContext를 호출하는 순간 에러가 발생합니다. 결국 이를 해결하려면 해당 컴포넌트를 불가피하게 'use client'로 전환해야 하는데, Root에 거대한 Provider를 두면 그 하위에 있는 수많은 컴포넌트들이 데이터를 읽기 위해 연쇄적으로 Client Component로 변질될 위험성, 즉 Client Boundary가 확장될 가능성이 커집니다.
물론 다크모드나 로그인 상태처럼 불가피하게 Root Layout을 감싸야 하는 전역 Provider도 존재하지만, 강의에서 경계한 것은 습관적인 Root Provider 래핑을 지양하자는 의미였습니다. 이러한 문제를 방지하고 Server Component의 이점을 극대화하기 위해 실무에서는 크게 두 가지 전략을 사용합니다.
첫 번째 전략은 Provider를 상태가 필요한 트리 최하단으로 밀어내는 것입니다. 전역 상태가 아니라 특정 도메인이나 UI 영역에서만 필요한 상태라면, 필요 없는 페이지까지 상태에 노출되도록 최상단 Layout을 감싸는 것이 아니라 해당 상태를 소비하는 가장 가까운 노드에서 Provider를 감싸야 합니다. 예를 들어 장바구니 기능이 필요한 특정 페이지나 레이아웃에서만 CartProvider로 랩핑하는 식입니다.
두 번째 전략은 Server Component에서 Client Component로 상태를 주입하는 Hydration 패턴을 사용하는 것입니다. RSC의 강점을 살리면서 클라이언트 상태 관리를 매끄럽게 하려면, 데이터 페칭은 서버에서 처리하고 그 초기값을 Zustand나 Jotai 같은 클라이언트 스토어에 주입하는 방식을 많이 사용합니다.
// Server Component
import { fetchUserSession } from '@/lib/api';
import ClientStoreProvider from './ClientStoreProvider';
export default async function UserDashboard() {
const sessionData = await fetchUserSession();
return (
<ClientStoreProvider initialSession={sessionData}>
<DashboardInteractiveUI />
</ClientStoreProvider>
);
}
이 예시처럼 서버에서 데이터 패칭을 진행하여 초기 상태를 Client Provider에 Prop으로 전달하게 되면, 보안에 민감한 데이터나 무거운 페칭 로직은 서버에 남겨두고 클라이언트에서는 가벼운 인터랙션 상태만 관리하는 이상적인 분리가 가능해집니다.
이와 관련하여 실무에서 RSC 아키텍처의 장점을 극대화하기 위해 적극적으로 권장하는 접근 방식과 라이브러리도 몇 가지 덧붙여 드립니다. 우선 서버와 클라이언트의 상태 동기화를 위해 복잡한 데이터 패칭과 캐싱이 필요한 경우에는 TanStack Query(구 React Query)의 HydrationBoundary 패턴을 추천합니다. 서버에서 데이터를 프리패치하여 클라이언트로 직렬화해 넘겨주면, RSC의 초기 렌더링 이점을 챙기면서도 클라이언트에서의 데이터 동기화를 매끄럽게 처리할 수 있습니다. 다음으로 경량화된 클라이언트 전역 상태를 다룰 때는 앞서 예시로 언급한 Zustand나 Jotai를 활용하는 것이 좋습니다.
이 라이브러리들은 React Context API 특유의 불필요한 리렌더링 이슈를 피할 수 있고, 트리 외부에서 상태를 관리할 수 있어 컴포넌트를 습관적으로 'use client'로 만드는 일을 줄여줍니다. 마지막으로 탭, 필터링, 정렬, 검색어 같은 상태는 전역 스토어 대신 URL Search Params를 활용하여 URL에 저장하는 방식을 권장합니다. 최근 nuqs 같은 라이브러리를 활용하면 Client Boundary를 넓히지 않고도 Server Component가 URL 파라미터를 읽어 렌더링을 제어하는 구조를 쉽게 구현할 수 있어 상태 관리의 복잡도를 크게 낮출 수 있습니다.
결론적으로 Eden님이 짚어주신 Composition Pattern은 Next.js App Router 생태계에서 중요하게 활용되는 패턴이며, 강의의 해당 표현은 Provider 사용 시 필연적으로 따라오는 데이터 소비의 부작용과 Client Component의 연쇄적 전파를 경계하자는 의미로 받아들여 주시면 좋을 것 같습니다.
좋은 질문 남겨주셔서 감사드립니다. 추가로 우리동네코딩스튜디오에서 관심 있는 강의가 있으시다면 jeony0535@naver.com 이메일로 연락해 주시면 할인 쿠폰을 전달해 드리겠습니다!
1
코드 예시까지 곁들여 자세히 설명해 주셔서 감사합니다. 덕분에 헷갈렸던 부분이 명확히 정리되었습니다.
제가 처음에 가졌던 의문은 "Composition 패턴을 쓰면 RSC가 유지되는데 왜 Provider 사용을 경계하라고 하셨을까?"였는데, 답변을 읽고 나니 강의에서 강조하신 핵심이 단순히 "Provider를 많이 쓰지 마세요" 가 아니라, "Client 상태 관리 범위를 최소화해서 Server Component 영역을 최대한 보존하라" 는 메시지였다는 것을 이해했습니다.
특히 Composition 패턴으로 RSC 자체는 유지되더라도, Context를 소비하는 순간 하위 컴포넌트가 연쇄적으로 'use client'로 전환되면서 Client Boundary가 확장될 수 있다는 점, 그리고 이를 방지하기 위해 Provider를 트리 최하단으로 밀어내거나 Zustand/Jotai, URL Search Params(nuqs) 같은 대안을 활용한다는 실무 전략까지 알려주셔서 큰 도움이 되었습니다.
다시 한 번 친절한 답변 감사드립니다.
0
AI 답변 감사합니다. 다만 답변 내용 중 "Provider가 클라이언트 컴포넌트라면 그 안의 모든 자식 컴포넌트도 클라이언트 환경에서 실행된다"는 부분이 제가 알고 있는 내용과 차이가 있어 추가로 여쭙고자 합니다.
제가 이해한 바로는, Client Component가 직접 import한 컴포넌트는 Client 경계에 포함되지만, children prop으로 외부에서 주입된 컴포넌트는 부모(Server Component)에서 이미 렌더링이 끝난 결과가 슬롯으로 들어가는 것이라 Server Component 상태를 유지하는 것으로 알고 있습니다.
이 composition pattern을 사용하면 Provider를 쓰면서도 children 영역은 RSC로 유지할 수 있어서, 실습 자료의 "정밀 아키텍처 분석"에 적힌 "App.tsx를 10개의 Provider로 감싸는 순간 그 아래의 모든 컴포넌트는 서버 컴포넌트로서의 최적화 기회를 잃게 됩니다"라는 설명이 모든 경우에 해당하지는 않는 것 같습니다.
혹시 강의에서는 composition pattern을 적용하지 않은 일반적인 경우를 전제로 설명해주신 건지, 지식공유자님의 의견이 궁금합니다.
(참고로 AI 인턴이 첨부해준 링크는 404 페이지로 연결되어 답변에 직접 관련된 자료는 아닌 것 같습니다.)
0
안녕하세요, 인프런 AI 인턴입니다. Eden님.
이 질문은 React 서버 컴포넌트(RSC)와 클라이언트 컴포넌트 간의 상호작용에 대한 것 같습니다. 일반적으로 Providers는 상태 관리나 테마 처리를 위한 Context를 제공하기 때문에 대개 클라이언트 컴포넌트로 구현됩니다. 이는 Providers 내부의 모든 컴포넌트를 클라이언트 환경에서 렌더링하게 만듭니다.
즉, Provider가 클라이언트 컴포넌트라면, 그 안에 있는 모든 자식 컴포넌트들도 클라이언트 환경에서 실행되어야 하며, 이는 서버 컴포넌트로써의 최적화, 즉 데이터 패칭과 초기 렌더링 성능 측면에서의 이점을 잃을 수 있음을 의미합니다.
하지만 Next.js 13의 새로운 app 디렉토리 구조에서는 서버 컴포넌트와 클라이언트 컴포넌트 간의 경계를 유지하는 것이 가능하며, 디자인 패턴에 따라 자식 컴포넌트를 서버 컴포넌트로 재사용하는 방법도 가능합니다. 이 경우 실제로 놓치고 있는 최적화 요소는 자식 컴포넌트가 클라이언트 컴포넌트로 지정된 상황에서 발생하는 추가적인 렌더링이나 데이터 패칭 지연입니다.
강의의 해당 방식이 특별히 어떤 조건 하에서 최적화 기회를 잃는다고 언급된 것인지, 혹은 다른 정확한 맥락이 있는지에 대해서는 아래 링크를 통해 강의 내용 및 토론을 확인해 보시는 것을 추천드립니다:
- Next.js의 서버 컴포넌트와 클라이언트 컴포넌트 간의 최적화 방법
저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해 드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.
우측 클로드 로고가 안떠요
0
10
1
강의 듣는 순서가 어떻게 되나요?
0
10
1
[2회독] plan mode / task-manager / superpowers 등등
0
18
1
[2회독]Warp 사용문의
0
14
1
강의 내용 부분이 잘못 올려진 거 같아요!
0
13
0
이전 강의에서는 /init을 토큰사용량 이슈로 '보는것'만 제안 주셨는데요..
0
14
1
스타터킷에 등록한 기술스택의 업그레이드 발생 시 방법
0
27
1
context7 mcp 설치 관련
0
25
0
context mcp tools 사용량 관련
0
17
0
플랜을 짠 다음 model를 변경해서 개발진행하는 방법문의
0
21
1
stopPropagation()에 대해서 질문 있습니다.
0
23
2
미션6 진행 관련 , /config -> output style 선택항목에 보이지 않는데...
0
19
2
요구사항인터뷰 강의자료 안보임
0
20
2
4강 강의 교안 소스코드
0
32
1
한글 밀림현상
0
33
2
cursor 단축키 관련 문의
0
28
2
커리큘럼 비활성화
0
21
2
12.13) 하단 여백 스타일링 관련 질문 드립니다.
0
31
2
Supabase 프로젝트 Claude 초기화 질문
0
29
2
화면 안 나옴
0
27
1
설명하시는 개념들을 잘 모르겠습니다.
0
21
2
문의관련 문의
0
65
2
[실습] 80강 없음
0
79
2
63,65 중복
0
119
1

