“좋아, 서버 컴포넌트와 클라이언트 컴포넌트의 개념은 알겠어. 그런데 이걸 우리가 개발할 때 어떻게 구분하고 사용해야 하는 거지?”
프로토타입이 점점 발전하면서, 팀 내에서 실용적인 질문들이 터져 나오기 시작했다. 개발자가 컴포넌트를 만들 때, 이것이 서버에서 실행될 것인지, 클라이언트에서 실행될 것인지를 React에게 어떻게 알려줄 수 있을까?
초기 논의는 파일 확장자를 사용하는 방향으로 흘러갔다.
“가장 명시적인 방법은 파일명으로 구분하는 겁니다.” 조쉬 스토리가 제안했다. “서버 컴포넌트는 MyComponent.server.js
로, 클라이언트 컴포넌트는 MyComponent.client.js
로 만드는 거죠. 확장자만 봐도 이 컴포넌트의 실행 환경을 즉시 알 수 있으니 직관적입니다.”
꽤 합리적인 아이디어였다. 이 방식은 React가 빌드 과정에서 파일을 분석하여, .server.js
파일에 포함된 코드는 클라이언트 번들에서 제외하고, .client.js
파일의 코드만 포함시키는 작업을 명확하게 수행할 수 있게 해줬다.
팀은 이 파일 확장자 규칙을 기반으로 내부 도구들을 만들기 시작했다. 서버 컴포넌트 파일 안에서 useState
를 사용하려고 하면 린터(Linter)가 즉시 경고를 띄웠고, 클라이언트 컴포넌트 파일 안에서 fs
모듈을 import
하려고 하면 에러가 발생했다. 이 규칙은 개발자에게 두 세계의 경계를 명확히 인지시키는 훌륭한 가드레일 역할을 했다.
하지만 이 방식에도 한계는 있었다.
며칠 후, 로렌 탄이 고개를 갸웃하며 문제를 제기했다.
“하나의 파일 안에 여러 개의 컴포넌트를 정의하는 경우는 어떻게 처리해야 할까요? 예를 들어, Buttons.js
파일 안에 PrimaryButton
과 SecondaryButton
을 함께 만들었는데, PrimaryButton
은 상호작용이 필요해서 클라이언트 컴포넌트여야 하고, SecondaryButton
은 단순 링크라서 서버 컴포넌트로도 충분하다면요?”
그녀의 지적에 모두가 잠시 생각에 잠겼다. 파일 단위로 세계를 나누는 방식은 이런 유연한 케이스를 처리할 수 없었다. Buttons.js
를 .client.js
로 만드는 순간, 그 안의 SecondaryButton
까지 불필요하게 클라이언트 컴포넌트가 되어버리는 것이다.
“파일 단위의 경계는 너무 경직되어 있어.” 앤드류가 결론 내렸다. “우리는 더 세밀한 제어, 즉 컴포넌트 단위가 아닌 파일 단위의 제어가 필요해.”
이 논의는 새로운 아이디어로 이어졌다. 파일 확장자라는 물리적인 약속 대신, 코드 안에 직접 명시하는 논리적인 약속을 만드는 것이었다.
“파일 최상단에 지시어(Directive)를 넣는 건 어떨까?”
한 엔지니어가 말했다. 마치 자바스크립트 초창기에 엄격 모드(Strict Mode)를 사용하기 위해 'use strict';
를 썼던 것처럼.
'use client';
이 한 줄의 문자열. 이 지시어가 파일의 맨 위에 선언되어 있다면, 그 파일 안에 있는 모든 컴포넌트는 클라이언트 컴포넌트로 간주된다. 반대로, 이 지시어가 없다면 기본적으로 서버 컴포넌트로 취급하는 것이다.
이 방식은 로렌이 제기했던 문제를 깔끔하게 해결했다. PrimaryButton
과 SecondaryButton
을 별개의 파일로 분리하고, PrimaryButton.js
파일에만 'use client';
를 선언하면 됐다.
이 ‘지시어’ 방식은 초기 아이디어였던 파일 확장자 방식보다 훨씬 더 유연하고 확장 가능성이 높았다. 나중에는 컴포넌트 단위가 아닌, 특정 함수만을 서버에서 실행하기 위한 'use server';
라는 개념으로까지 발전할 수 있는 씨앗을 품고 있었다.
React Core Team은 파일 확장자라는 초기 아이디어를 버리고, 지시어를 사용하는 방식으로 방향을 틀었다. 이것은 단순한 문법의 변경이 아니었다. 경직된 규칙에서 유연한 선언으로, 개발자에게 더 많은 제어권과 명확성을 부여하는 방향으로 진화하는 과정이었다.
'use client'
. 이 짧은 문자열은 앞으로 두 개의 세계를 나누는 명확한 국경선이 될 터였다.