파일명의 약속, .server.js

142025년 08월 29일3

“좋아, 서버 컴포넌트와 클라이언트 컴포넌트의 개념은 알겠어. 그런데 이걸 우리가 개발할 때 어떻게 구분하고 사용해야 하는 거지?”

프로토타입이 점점 발전하면서, 팀 내에서 실용적인 질문들이 터져 나오기 시작했다. 개발자가 컴포넌트를 만들 때, 이것이 서버에서 실행될 것인지, 클라이언트에서 실행될 것인지를 React에게 어떻게 알려줄 수 있을까?

초기 논의는 파일 확장자를 사용하는 방향으로 흘러갔다.

“가장 명시적인 방법은 파일명으로 구분하는 겁니다.” 조쉬 스토리가 제안했다. “서버 컴포넌트는 MyComponent.server.js로, 클라이언트 컴포넌트는 MyComponent.client.js로 만드는 거죠. 확장자만 봐도 이 컴포넌트의 실행 환경을 즉시 알 수 있으니 직관적입니다.”

꽤 합리적인 아이디어였다. 이 방식은 React가 빌드 과정에서 파일을 분석하여, .server.js 파일에 포함된 코드는 클라이언트 번들에서 제외하고, .client.js 파일의 코드만 포함시키는 작업을 명확하게 수행할 수 있게 해줬다.

팀은 이 파일 확장자 규칙을 기반으로 내부 도구들을 만들기 시작했다. 서버 컴포넌트 파일 안에서 useState를 사용하려고 하면 린터(Linter)가 즉시 경고를 띄웠고, 클라이언트 컴포넌트 파일 안에서 fs 모듈을 import하려고 하면 에러가 발생했다. 이 규칙은 개발자에게 두 세계의 경계를 명확히 인지시키는 훌륭한 가드레일 역할을 했다.

하지만 이 방식에도 한계는 있었다.

며칠 후, 로렌 탄이 고개를 갸웃하며 문제를 제기했다.

“하나의 파일 안에 여러 개의 컴포넌트를 정의하는 경우는 어떻게 처리해야 할까요? 예를 들어, Buttons.js 파일 안에 PrimaryButtonSecondaryButton을 함께 만들었는데, PrimaryButton은 상호작용이 필요해서 클라이언트 컴포넌트여야 하고, SecondaryButton은 단순 링크라서 서버 컴포넌트로도 충분하다면요?”

그녀의 지적에 모두가 잠시 생각에 잠겼다. 파일 단위로 세계를 나누는 방식은 이런 유연한 케이스를 처리할 수 없었다. Buttons.js.client.js로 만드는 순간, 그 안의 SecondaryButton까지 불필요하게 클라이언트 컴포넌트가 되어버리는 것이다.

“파일 단위의 경계는 너무 경직되어 있어.” 앤드류가 결론 내렸다. “우리는 더 세밀한 제어, 즉 컴포넌트 단위가 아닌 파일 단위의 제어가 필요해.”

이 논의는 새로운 아이디어로 이어졌다. 파일 확장자라는 물리적인 약속 대신, 코드 안에 직접 명시하는 논리적인 약속을 만드는 것이었다.

“파일 최상단에 지시어(Directive)를 넣는 건 어떨까?”

한 엔지니어가 말했다. 마치 자바스크립트 초창기에 엄격 모드(Strict Mode)를 사용하기 위해 'use strict';를 썼던 것처럼.

'use client';

이 한 줄의 문자열. 이 지시어가 파일의 맨 위에 선언되어 있다면, 그 파일 안에 있는 모든 컴포넌트는 클라이언트 컴포넌트로 간주된다. 반대로, 이 지시어가 없다면 기본적으로 서버 컴포넌트로 취급하는 것이다.

이 방식은 로렌이 제기했던 문제를 깔끔하게 해결했다. PrimaryButtonSecondaryButton을 별개의 파일로 분리하고, PrimaryButton.js 파일에만 'use client';를 선언하면 됐다.

이 ‘지시어’ 방식은 초기 아이디어였던 파일 확장자 방식보다 훨씬 더 유연하고 확장 가능성이 높았다. 나중에는 컴포넌트 단위가 아닌, 특정 함수만을 서버에서 실행하기 위한 'use server';라는 개념으로까지 발전할 수 있는 씨앗을 품고 있었다.

React Core Team은 파일 확장자라는 초기 아이디어를 버리고, 지시어를 사용하는 방식으로 방향을 틀었다. 이것은 단순한 문법의 변경이 아니었다. 경직된 규칙에서 유연한 선언으로, 개발자에게 더 많은 제어권과 명확성을 부여하는 방향으로 진화하는 과정이었다.

'use client'. 이 짧은 문자열은 앞으로 두 개의 세계를 나누는 명확한 국경선이 될 터였다.