Vercel Next.js - Open Source Contribution
Next.js의 서버 컴포넌트 동작 방식이 궁금해 관련 코드를 살펴보던 중, server.ts에서 서버 컴포넌트 환경에서 특정 React API의 사용을 제한하는 로직을 확인했다.
// packages/next/src/server/typescript/constant.ts
export const DISALLOWED_SERVER_REACT_APIS: string[] = [
'useState',
'useEffect',
...
'useOptimistic',
'useActionState',
]
// packages/next/src/server/typescript/rules/server.ts
if (DISALLOWED_SERVER_REACT_APIS.includes(name)) {
diagnostics.push({ messageText: `"${name}" is not allowed in Server Components.` })
}그런데 DISALLOWED_SERVER_REACT_APIS 목록에 useMemo, useCallback, useEffectEvent는 포함되어 있지 않았다.
서버 컴포넌트는 요청 시 서버에서 실행되어 결과가 RSC Payload(직렬화된 JSON 구조) 형태로 전달되며, 클라이언트 컴포넌트처럼 리렌더링이 발생하지 않는다.
따라서 서버 컴포넌트 내에서 useMemo나 useCallback을 사용하는 것은 매 요청마다 컴포넌트가 새로 호출되는 서버의 특성상 성능최적화 관점에서 의미가 없다.
그럼에도 불구하고 제한되지 않은 이유가 무엇인지 궁금해졌고, 이벤트와 렌더링 분리를 목적으로 도입된 useEffectEvent가 없는 이유도 생각해봤을 때 Next.js가 서버 컴포넌트에서 React API를 제한하는 기준이 무엇인지 궁금하여 시작됐다.
useMemo와 useCallback은 왜 선언이 가능한가?
Next.js의 DISALLOWED_SERVER_REACT_APIS 목록에 useMemo와 useCallback이 포함되지 않은 이유는 React 서버 컴포넌트 구현체가 이 Hook들을 아무런 동작도 하지 않는(No-op) 상태로 처리하고 있기 때문이다.
즉, 서버 환경에서는 단순히 인자로 받은 함수나 값을 그대로 반환하도록 되어있다.
// React Server - packages/react-server/src/ReactFlightHooks.js
export const Dispatcher: DispatcherType = {
useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
return create();
},
useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
return callback;
},
// ...
};React 내부 코드를 보면 서버용 Dispatcher는 의존성 배열을 비교하거나 값을 메모리상에 유지하는 로직이 생략되어 있다.
// React Client - packages/react/src/ReactHooks.js
function resolveDispatcher() {
const dispatcher = ReactSharedInternals.H;
// ...
return ((dispatcher: any): Dispatcher);
}
export function useCallback<T>(
callback: T,
deps: Array<mixed> | void | null,
): T {
const dispatcher = resolveDispatcher();
return dispatcher.useCallback(callback, deps);
}
export function useMemo<T>(
create: () => T,
deps: Array<mixed> | void | null,
): T {
const dispatcher = resolveDispatcher();
return dispatcher.useMemo(create, deps);
}반면, 클라이언트 환경의 Hook은 현재 렌더링 상태를 관리하는 Dispatcher에 의존하며 복잡한 생명주기 로직을 수행한다.
클라이언트용 Dispatcher는 서버와 달리 이전 렌더링의 의존성 비교, 메모리 내 값 보존, 리렌더링 시 최적화 로직 등을 모두 포함하여 동작한다.
결국 React 서버 구현체 내에서 이 Hook들이 이미 아무런 동작도 하지 않는(No-op) 상태로 처리되고 있기에 Next.js도 이를 별도의 에러로 분류하지 않은 것이 아닐까 싶은 생각이다.
useEffectEvent는 왜 금지해야 하는가?
useMemo와 useCallback과 달리 useEffectEvent는 useEffect내 의존성 관리를 위해 생긴 것이기에 서버에서 허용하는 것은 설계 원칙과 일관성 측면에서 어긋난다고 생각한다.
따라서 useEffectEvent에 대한 것만 Pull request를 올리기로 결정했다.
첫 번째 Pull Request 실패
깔끔하게 잘 마무리하고 싶었다..
하지만, 리뷰 과정에서 수정해야 할 부분이 더 있다는 피드백을 받았다. 😂

어디가 수정되어야 하는지 다시 살펴보았고, 나는 Pull Request에서 가장 중요한 것도 놓쳤다..
가독성 좋은 코드와 설명으로 리뷰어의 시간을 절약하는 것
결국 처음 올렸던 Pull Request를 닫았다.
두 번째 Pull Request 성공
놓쳤던 코드를 수정한 뒤 다시 Pull Request를 올렸다.
하지만, 이번에는 이전과 다르게 내가 리뷰어가 되었다고 생각하며 확실하게 작업했다.
- 간결하게 정리한 내용
- 예상 결과
- 현재 동작 방식과 기대 동작의 차이
- 어떤 코드를 작업했는지
모든 스타일을 바꿨다.

그 결과, 이전과는 다르게 좋은 코멘트를 받을 수 있었다.
두 번째 오픈소스 기여였는데 React와 Next.js의 코드 분석도 좋았지만, 다른 사람이 남긴 히스토리를 추적하며 코드와 스타일, 오픈소스 컨트리뷰션 규칙 등을 지키면서 작업하니 회사에서 경험했던 협업과는 또 다른 느낌이었다.