면접을 보면서 어떤 질문을 받았는지에 대해서는 얘기할 수 없으나 찬찬히 곱씹어 보니 이 Server Component에 대한 얘기를 듣고싶었던 것 같다. 다만 사용해보지 않아 자세한 답변을 못드렸기에 이 참에 더 알아보자.
일반적으로 데이터를 받아오는 방법에는 2가지가 있다.
1번의 경우는 부모와 자식 간의 의존성이 심해져 유지 보수가 어려워진다라는 단점이 있다. 그리고 부모 기준으로 설계된 API는 컴포넌트의 구성이 바뀌거나 자식 컴포넌트가 다른 컴포넌트로 이동되는 경우 그 API를 다른 컴포넌트에서 호출해줘야 하기에 불필요한 정보의 over-fetching이 있을 수 있다.
물론 tanstack-query를 이용해 server-state를 캐싱하고 여러번 호출되지 않고 기존의 데이터를 가져오는 것으로 해결은 가능하나 현재 기준으로는 react-query를 쓰지 않는다라고 가정해본다.
2번의 경우 컴포넌트 렌더링 시점에서만 필요한 데이터를 가져올 수 있다는 장점이 있으나 클라이언트에서 서버 요청이 증가하고 Suspense를 활용하게 된다면 그 API들이 전부 호출 완료될 때 까지 기달려야한다라는 단점이 생긴다.
물론 이 경우도 컴포넌트 별 Suspense를 걸어두는 것도 하나의 방법이 될 수 있으나 모든 정보를 한번에 보는 경우라면 모든 API 콜의 응답을 기다려야만 한다. 그리고 그것은 렌더 지연으로도 이어진다.
어찌되었든 Client Side에서의 컴포넌트 안에 API를 호출한다 라는 것은 크게 2가지 문제가 있다라는 것이 된다.
클라이언트 내부에서 useEffect로 API를 호출해 데이터를 반영하게 된다면 loading 상태에 따라 UI를 다르게 보여주는 렌더링과, loading이 완료되었을 때 보여주는 렌더링 2번의 렌더링 작업이 필요하다.
결국은 리렌더링을 요청하게 된다라는 문제가 있어 client-server-waterfall을 야기하고 성능을 저하시키는 원인이 된다.
서버 컴포넌트는 말 그대로 서버에서 동작하는 컴포넌트로 기존 Client-Component의 문제를 해결해주기 위해 출시되었다.
컴포넌트의 렌더링을 클라이언트가 아닌 서버 단계에서 수행할 수 있기 때문에 API 요청에 대한 데이터 요청을 줄일 수 있고, 클라이언트에서도 연속된 호출을 줄일 수 있어 client-server-waterfall을 줄일 수 있고, 여러가지 이점을 얻을 수 있다.
서버 컴포넌트는 서버에서 동작하기에 DB, File System(I/O) 그리고 인터널 서비스 같은 Server Side에 접근할 수 있다.
그리고 미리 fetching을 진행해 그 데이터를 클라이언트 컴포넌트에 props로 전달하는 것이 가능하다.
(데이터는 json 기반의 인코딩 가능한 serializable props만 가능하다. function은 안됨)
서버 컴포넌트 코드는 브라우저에서 다운로드 되는 것이 아닌 서버 측에서 미리 렌더링 된 static한 컨텐츠를 전달하기 때문에 패키지를 추가해도 번들 사이즈에 영향을 끼치지는 않는다라는 이점이 있다.
그러기 때문에 유저 인터렉션이 없는 컴포넌트의 경우 서버 컴포넌트로 마이그레이션을 진행하게 된다면 동일한 View를 제공함과 동시에 번들 사이즈와 초기 로딩 시간을 감소하는데 도움이 된다.
기존에 코드 분할은 React.lazy 혹은 dynamic import를 활용해 렌더링에 필요한 컴포넌트를 동적으로 불러오는 작업을 진행하였다.
Code Spliting으로 앱의 퍼포먼스를 상승시킬 수는 있으나 lazy loading이 필요한 컴포넌트마다 일일히 lazy loading을 설정해야한다는 단점과 부모 컴포넌트가 렌더링이 되어야만 로딩을 시작한다라는 문제가 있다.
서버 컴포넌트에서는 이것을 2가지 방법으로 해결한다.
이 2가지 방법으로 미리 명시하지 않으며 렌더링 프로세스 초기에 번들을 다운로드해 사용한다 라는 이점을 얻을 수 있다.
서버 컴포넌트의 도입으로 리액트 컴포넌트는 3가지 컴포넌트로 분류되게 되었다. React 18 이전의 컴포넌트들은 클라이언트 컴포넌트로 분류되고, 서버, 공유 컴포넌트 2종류가 추가되었다.
타입 | 설명 | 주의사항 | 파일 네임 컨벤션 |
---|---|---|---|
서버 컴포넌트 | - 서버에서만 렌더링되는 컴포넌트 - 유저 인터렉티비티 제공 불가 | - state, effect같은 리렌더링 문법 사용 불가능 - DOM, 브라우저 API 사용 불가 - Server Only만 접근, 데이터 사용 가능 - 클라이언트 컴포넌트 import 및 렌더링 가능 - 클라이언트 컴포넌트 props로 serializable한 데이터 전달 가능 | test.server.js |
클라이언트 컴포넌트 | - 클라이언트에서 렌더링 되거나 SSR을 통해 서버에서 렌더링 되는 컴포넌트 - 유저 인터랙션 사용 가능 - 서버 컴포넌트 도입 전 리액트 컴포넌트 | - 서버 컴포넌트 import 불가 - 서버 컴포넌트에서 또 다른 클라이언트 컴포넌트에게 또 다른 서버 컴포넌트를 넘겨주는 것은 가능 <ClientComp><OtherServerComp /></ClientComp> - server only 데이터 사용 불가 - state, effect, 브라우저 API 사용 가능 | test.client.js |
공유 컴포넌트 | - 서버와 클라이언트에서 렌더링 되는 컴포넌트 | - server only 데이터 사용, 서버 컴포넌트 import 불가 - state, effect, 브라우저 API 사용 불가 서버와 클라이언트 컴포넌트에서 import 가능 | test.js |
어찌보면 위의 기능들 즉 서버를 통해 데이터를 바로 접근하고 렌더링하는 방식은 마치 SSR과 비슷한 느낌 또한 든다. 그리고 헷갈리는 개념이 될 수도 있다.
하지만 중요한 것은 서버 컴포넌트는 SSR의 대체가 아니다. UX 향상을 위해 같이 쓸 수 있는 개념으로 이해 해야한다.
CSR의 경우 앱 진입 전까지 HTML, JS, CSS 그리고 모든 데이터가 로드되고 렌더링이 끝나기 전까지 아무런 동작을 하지 못한다. 즉 JS 번들 사이즈와 네트워크 상황에 따라 UX가 결정될 수도 있다는 문제가 발생한다.
SSR은 서버에서 JS를 받아 HTML을 렌더링하고 서버에서 JS 번들을 전부 다운로드하고 hydration이 진행되어야 하지만, 그동안 빈 화면 대신 미리 데이터가 있는 HTML을 제공해 무거운 JS가 다운로드 되는 동안 유의미한 컨텐츠를 미리 제공해준다라는 이점이 있다
차이점을 생각해보면 다음과 같이 생각할 수 있다.
즉 SSR에서의 문제점 HTML refetch, 특정 컴포넌트 내의 서버 접근, JS 번들 전달 등의 문제를 해결하기 가장 쉬운 수단은 Server Component이며 기존 SSR에 Server Component를 일부 첨가함으로써 이점을 얻을 수 있게 된다.