Nextjs 13버전이 등장했다(고하자마자 14가 등장했다). 가장 큰 변경점인 app directory는 stable이 된지 얼마 되지 않았기에 app directory의 default값이 된 server component(RSC)에 대한 특징에 대한 논의는 많지만 앱 기술스택이나 스트럭처를 어떻게 구성할지에 대한 포스팅은 거의 없는 거 같다. 엔터급에서는 이륙허가를 위한 기술검토 중이거나 최적의 스트럭처를 위한 연구를 내부적으로 할 것이라 생각한다.
RSC를 메인으로 하는 Next13의 스트럭처를 구상하기 위해선 서버 컴포넌트에 대한 이해가 필수적이다.
CSR은 처음부터 index.html을 자바스크립트 번들과 말아서 한번에 렌더링합니다.
SSR은 자바스크립트와 관계없는 정적인 부분을 서버가 첫 렌더링으로 실행합니다. 덕분에 유저는 자바스크립트를 내려받기 전에도 화면의 뼈대를 미리 볼 수 있습니다. 이후 자바스크립트 번들의 다운이 완료되면 HTML을 자바스크립트에 따라 조작하는 Hydration 과정을 거칩니다. 아직까진 대부분의 로직을 클라이언트가 처리하고있고, 편의성을 위해 극히 일부만을 서버가 수행하고있는 모습입니다. WebAPI나 이벤트 핸들러 등의 자바스크립트들은 클라이언트에서 다운받고 실행되는 것 외의 플랜을 떠올리기 어렵습니다. 하지만 유저 상호작용이 없는 클라이언트 컴포넌트, 데이터 페칭 등 서버가 충분히 비벼볼만한 작업들을 아직도 클라이언트가 하고있는 모습입니다.
RSC는 서버가 비벼볼 만한 작업들을 모두 서버에서 미리 실행하는 모습입니다. 클라이언트 컴포넌트에서 하고있던 작업들 중 서버가 비벼볼만 한 것들을 모두 서버로 가져왔습니다. 클라이언트가 해야 할 일이 상당히 줄어들었습니다. 그러나....
한 컴포넌트가 하던 일을 서버/클라이언트로 분리했고, 둘이 실행되는 환경도 달라졌습니다. 리액트 컴포넌트를 만들어내는 과정도 서버에서 한번, 클라이언트에서 한번 실행되고 있습니다. 여기서 직렬화라는 용어가 등장합니다. 리액트의 컴포넌트 트리에 createElement같은 리액트 컴포넌트 요소로 반영되는 과정을 서버에서도 한번 거치고 클라이언트로 전달되게 됩니다. 이때 서버컴포넌트는 html 태그를 가진 리액트 컴포넌트로 변환되고, 클라이언트 컴포넌트는 placeholder로 남아 클라이언트에게 렌더링을 위임하게 됩니다. 따라서 직렬화 가능한 요소와 직렬화 불가능한 요소 간의 불꽃튀는 신경전이 발생합니다.
여기서 Nextjs에서 설명하는 중요한 패턴을 관찰할 수 있습니다.
ex)
'use client'
// You cannot import a Server Component into a Client Component.
import ServerComponent from './Server-Component'
export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ServerComponent />
</>
)
}
ex)
'use client'
import { useState } from 'react'
export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
{children}
</>
)
}
이런 배경으로 인해 각 컴포넌트에서 할 수 있는 역할은 다음과 같이 분리됩니다.
사실 Next13에 대한 좋은 정리는 너무나 많지만 이런 글을 쓰는 이유는 어떤 툴로 어떤 스트럭처를 구성할 것인가?에 대한 한국어로 된 논의는 거의 전무한 듯 해서 입니다. 디자인 시스템이 어느정도 있는 채로 새로 진행하게 된 프로젝트에서 Next13을 이용하기로 했고, 기존에 Styled-Components로 작성한 UI컴포넌트를 최대한 재사용 하기위해 Styled-Components를 그대로 가져가기로 했습니다.
Nextjs의 설명에 따르면 확실히 상호작용이 필요한 클라이언트 컴포넌트가 필요한 부분은 대부분의 페이지에서 소수에 속합니다. 그렇기에 레이아웃을 서버 컴포넌트로 구성할 것을 권하고 있습니다.
Styled Components는 런타임 CSS in JS로 JS번들을 통해 CSS를 생성합니다. 글로벌 테마를 일관성 있게 관리할 수 있다는 최대의 장점으로 인해 root layout을 클라이언트 컴포넌트로 두면서 provider를 배치하는게 거의 반 강제되기에, Styled Components를 채택한 순간부터 저 설계 권장사항과는 대립하게됩니다. 사실상 대부분 컴포넌트가 클라이언트 컴포넌트로 구성되어, 라우팅 체계 외에는 별 차이가 없게 되는 것입니다.
실제로 서버 컴포넌트를 메인으로 두고 클라이언트 컴포넌트를 말단에 두려는 시도를 해봤을 때, 이벤트 핸들러, form validation 등 서버 컴포넌트를 사용할 수 없는 상황이 너무나 많이 존재한다고 느꼈습니다. form은 공식페이지에서 server action과 함께 몇 페이지를 할애하며 장황하게 설명하고 있지만, form도 상호작용이 들어가게 된다면 결국 클라이언트 컴포넌트를 통해 react hook form의 도움을 받아야하죠.
클라이언트 컴포넌트가 늘어감에 따라 fetch도 처음의 계획과는 다른길을 가게 됩니다. 서버 컴포넌트에서 모두가 놀라워하는 부분이었던 '컴포넌트 함수내에서 이펙트함수 없이 node fetch를 통한 데이터페칭'을 거의 사용할 수 없게되었고, 결국 react query를 사용하게 되었습니다.
CSS framework 혹은 Zero-runtime CSS in JS를 채택했다면?
빌드타임에 css를 생성하는 Zero-runtime CSS in JS 솔루션들은 대부분 서버 컴포넌트와 호환이 가능합니다. 처음부터 vanilla extract같은 솔루션을 채택했다면, 서버컴포넌트와 클라이언트 컴포넌트가 상당 수 공존하는 형태가 될 것입니다. 다만 이 경우 컴포넌트 별로 다른 데이터 페칭 솔루션을 가져가야 하기에 코드 복잡도가 증가할 수 있는 점을 고려해야 할 것입니다. (클라이언트에서의 fetch와 서버 컴포넌트의 node fetch) 클라이언트 컴포넌트의 파이가 크고, 구성원들이 런타임css in js 솔루션에 익숙한 조직이라면, 해당 솔루션을 자신있게 소개하기 힘들 것입니다.
라우팅 별로 layout이 최상단에 배치되는 특성에 따라 코딩을 하다보면 자연스럽게 app 디렉토리 내 폴더 배치는 layout을 기준으로 배치되게 됩니다. 이를테면
.
└── app/
├── (blueLayout)/
│ ├── home/
│ │ └── layout.tsx
│ ├── intro/
│ │ └── layout.tsx
│ └── layout.tsx
└── (redLayout)/
├── signin/
│ └── layout.tsx
├── signup/
│ └── layout.tsx
└── layout.tsx
공통 레이아웃을 재사용하기 위해 라우팅을 레이아웃 별로 그루핑 하는 식입니다.
app directory의 사고흐름을 따라가다보니 자연스럽게 떠오른 발상인데, app 폴더를 펼쳤을 때
라우팅 리스트를 한눈에 파악하기가 다소 불편하다는 지적은 꽤나 정당해보입니다.
따라서 현재는
.
├── app/
│ ├── home/
│ │ ├── _components/
│ │ │ └── homeInput.tsx
│ │ └── layout.tsx
│ ├── intro/
│ │ └── layout.tsx
│ ├── signin/
│ │ └── layout.tsx
│ ├── signup/
│ │ └── layout.tsx
│ └── layout.tsx
├── layout/
│ ├── blueLayout.tsx
│ └── redLayout.tsx
└── components/
└── input.tsx
레이아웃을 별도의 폴더에 분리하여 import하는 방식으로 app directory를 펼쳤을 때 라우팅 리스트가 한번에 파악될 수 있도록 하였고, 해당 라우팅에서만 개별적으로 쓰이는 컴포넌트들을 private폴더(_components)로 분리하여 구성하는 방식을 채택하게 되었습니다.