원문 React Labs: What We've Been Working On – March 2023
이미지 출처: https://react.dev/images/og-blog.png
저자: Joseph Savona, Josh Story, Lauren Tan, Mengdi Chen, Samuel Susla, Sathya Gunasekaran, Sebastian Markbåge, Andrew Clark
리액트 실험실 블로그에서는 현재 연구하고 개발 중인 프로젝트에 대한 내용을 전달합니다. 지난 업데이트 이후 의미 있는 진전이 있었으며, 그에 대한 내용을 공유하고자 합니다.
리액트 서버 컴포넌트(또는 React Server Components, RSC)는 리액트 팀이 고안한 새로운 애플리케이션 아키텍처입니다.
우리는 먼저 소개 강연과 RFC에서 RSC에 대한 연구를 공유한 바 있습니다. 다시 되짚어보면, 미리 실행되어 자바스크립트 번들에서 제외되는 새로운 종류의 컴포넌트인 서버 컴포넌트를 소개했습니다. 서버 컴포넌트는 빌드 중에 실행되어 파일 시스템에서 읽어오거나 정적 콘텐츠를 가져오는 것이 가능합니다. 또한 서버에서 실행되는 것이 가능하므로 API를 빌드하지 않고도 데이터 계층에 접근할 수 있습니다. 프로퍼티를 통해 서버 컴포넌트로부터 브라우저의 인터랙티브한 클라이언트 컴포넌트로 데이터를 전달할 수 있습니다.
RSC는 서버 중심의 멀티 페이지 애플리케이션의 단순한 "요청/응답" 멘탈 모델과 클라이언트 중심의 단일 페이지 애플리케이션의 원활한 상호작용을 결합함으로써 두 모델의 장점을 모두 제공합니다.
마지막 업데이트 이후로, 우리는 RSC 제안에 대해 최종적으로 승인하고자 리액트 서버 컴포넌트 RFC를 머지했습니다. 리액트 서버 모듈 컨벤션 제안에 대해 처리되지 않은 문제들을 해결했고, "use client"
컨벤션을 따르기로 파트너들과 합의했습니다. 이 문서는 또한 RSC 호환성 구현이 지원해야 하는 것들에 대한 규격의 역할을 합니다.
가장 큰 변화는 서버 컴포넌트로부터 데이터를 가져오는 우선적인 방법으로 async
/await
방식을 도입했다는 점입니다. 또한 Promise를 언래핑하는 use
라는 훅을 도입하여 클라이언트에서 데이터 로딩하는 것을 지원할 계획입니다. 클라이언트 전용 애플리케이션의 임의의 컴포넌트에서는 async/await
기능을 지원할 순 없지만, RSC 애플리케이션과 유사하게 클라이언트 전용 애플리케이션을 구조화할 때 이를 지원할 수 있게끔 추가하려고 합니다.
이제 데이터 가져오기 기능이 어느 정도 잘 정리되었으므로, 다른 방향을 살펴보고 있습니다. 데이터베이스 뮤테이션을 실행하고 형태를 구현할 수 있도록 클라이언트에서 서버로 데이터를 보내는 것에 대한 것이죠. 서버/클라이언트 경계를 넘어 서버 액션 함수를 전달하면 클라이언트가 그 함수를 호출함으로써 원활한 RPC를 제공하고자 하는 목적입니다. 서버 액션은 또한 자바스크립트가 로드되기 전에 점진적으로 향상된 형태를 제공합니다.
리액트 서버 컴포넌트는 Next.js 애플리케이션 라우터에도 포함되었습니다. 이는 라우터의 깊은 결합을 통해 RSC가 원시적으로 소비되는 것을 보여주지만, RSC와 호환되는 라우터와 프레임워크를 구축하는 유일한 방법은 아닙니다. RSC의 사양에서 제공하는 기능과 구현은 명확하게 구분되어 있습니다. 리액트 서버 컴포넌트는 리액트 프레임워크에서 호환되어 동작하는 컴포넌트를 위한 하나의 명세입니다.
우리는 일반적으로 이미 존재하는 프레임워크를 사용하는 것을 권장하지만, 만약 본인만의 커스텀 프레임워크를 구축하고자 한다면 그것 또한 가능합니다. RSC와 호환가능한 프레임워크를 직접 구축하는 것은 번들러의 깊은 결합을 필요하기 때문에 생각만큼 쉽지 않습니다. 현재 사용되는 번들러들은 클라이언트에서 사용하기는 좋지만, 서버와 클라이언트 간에 단일 모듈 그래프를 분할하는 것에 대해서는 최우선으로 고안되지는 않았습니다. 이것이 RSC에 필요한 기본적인 요소들을 내장하고자 번들러 개발자들과 직접적인 파트너십을 맺고 있는 이유입니다.
서스펜스(Suspense)를 사용하여 컴포넌트의 데이터나 코드가 로드되는 동안 화면에 보여줄 내용을 지정할 수 있습니다. 이를 통해 페이지가 로드되는 동안 뿐만 아니라 더 많은 데이터와 코드를 로드하는 라우터 탐색 중에도 사용자들이 계속해서 더 많은 데이터를 볼 수 있게 됩니다. 그러나 사용자의 관점에서 볼 때, 새로운 콘텐츠가 준비되었는지 여부를 고려할 때 데이터 로딩과 렌더링은 모든 것을 알려주진 않습니다. 기본적으로 브라우저는 스타일시트, 폰트, 이미지들을 독립적으로 로드하기 때문에 UI 점프와 연속적인 레이아웃의 이동이 발생할 수 있습니다.
우리는 스타일시트, 폰트, 이미지의 로딩 라이프사이클과 서스펜스를 완전히 통합함으로써 리액트가 화면에 콘텐츠가 표시될 준비가 되었는지 판단할 수 있게끔 노력하고 있습니다. 리액트 컴포넌트를 작성하는 방식을 변경하지 않더라도, 화면의 갱신이 더 일관성 있고 만족스러운 방식으로 이뤄집니다. 최적화를 위해 폰트와 같은 애셋을 컴포넌트에서 직접 미리 로드하는 수동 방식도 제공할 예정입니다.
현재 이러한 기능들을 구현 중이며 곧 더 많은 기능들을 공유하겠습니다.
애플리케이션의 여러 페이지와 화면들은 아마 서로 다른 메타데이터를 가질 것입니다. <title>
태그, 설명, 화면과 관련된 다른 <meta>
태그와 같은 데이터들 말이죠. 유지보수의 관점에서 이 데이터는 관련 페이지나 화면에 대한 리액트 컴포넌트와 가까이 유지하는 편이 확장성이 더 높습니다. 그러나 메타데이터의 HTML 태그들은 일반적으로 애플리케이션의 최상위 컴포넌트에서 렌더링 되는 문서의 <head>
부분에 있어야 합니다.
오늘날 개발자들은 두 가지 방법 중 하나를 선택하여 이 문제를 해결하고 있습니다.
한 가지 방법은 <title>
, <meta>
및 다른 태그들을 문서 <head>
로 옮기는 특별한 서드파티 컴포넌트를 렌더링하는 것입니다. 이 방법은 주요 브라우저에서 동작하지만, 오픈 그래프 파서와 같은 클라이언트 사이드 자바스크립트를 실행하지 않는 클라이언트가 많기 때문에 보편적인 해결책은 아닙니다.
또 다른 방법으로는 페이지를 두 개로 나누어 서버 렌더링하는 것입니다. 먼저 주요 콘텐츠가 렌더링 되고 이러한 모든 태그를 수집합니다. 그 후, 수집된 태그들과 같이 <head>
를 렌더링 한 뒤 마지막으로 <head>
와 주요 콘텐츠를 브라우저로 전송합니다. 이 접근 방법은 잘 동작하지만, 리액트 버전 18의 스트리밍 서버 렌더러의 장점까지 막아버린다는 문제가 있습니다. <head>
를 전송하기 전에 모든 콘텐츠가 렌더링 될 때까지 기다려야 하기 때문입니다.
그래서 컴포넌트 트리 어느 곳에서든 <title>
, <meta>
, 그리고 메타데이터 <link>
태그를 즉시 렌더링할 수 있는 내장 지원을 추가하려고 합니다. 완전한 클라이언트 사이드 코드, SSR, 향후 RSC를 포함한 모든 환경에서 동일한 방식으로 동작할 것입니다. 이에 대한 자세한 내용은 조만간 공유해드리겠습니다.
지난 업데이트 이후 리액트를 위한 최적화 컴파일러인 React Forget에 대한 설계를 적극적으로 반복했습니다. 앞서 "자동 메모이징 컴파일러"라고 언급했었는데, 어떤 의미에선 사실입니다. 그러나 컴파일러를 구축하면서 리액트의 프로그래밍 모델을 더 깊게 이해할 수 있게 되면서, React Forget을 자동 메모이징 컴파일러 대신 자동 반응성 컴파일러로서 이해하는 것이 더 좋다고 생각했습니다.
리액트의 핵심 아이디어는 개발자가 현재 상태의 함수로 UI를 정의하는 것입니다. 숫자, 문자열, 배열, 객체 등 일반적인 자바스크립트 값으로 작업하고 if/else, for 등과 같은 표준 자바스크립트 관용구를 사용하여 컴포넌트 로직을 만들어 냅니다. 애플리케이션의 상태가 변경될 때마다 리액트가 리렌더링을 하는 멘탈 모델을 가집니다. 우리는 이렇게 단순한 멘탈 모델을 믿고 자바스크립트 의미론에 가깝게 유지하는 것이 리액트의 프로그래밍 모델에서 중요한 원칙이라고 생각하고 있습니다.
문제는 리액트가 때때로 너무 반응적이라 과하게 리렌더링을 할 수 있다는 것입니다. 예를 들어 자바스크립트에서는 두 객체나 배열이 동등한지(키와 값이 모두 같은지) 비용이 적게 들면서 비교하는 방법이 없기 때문에, 렌더링할 때마다 새로운 객체나 배열을 만들면 리액트가 필요한 것보다 더 많은 작업을 할 수도 있습니다. 즉, 개발자는 리액트가 변경 사항에 대해 과민하게 반응하지 않도록 컴포넌트를 명시적으로 메모해야만 합니다.
React Forget의 목표는 리액트 애플리케이션이 기본적으로 적당한 반응성을 갖는 것입니다. 즉, 상태 값이 유의미하게 변경될 때만 리렌더링이 일어나야 한다는 것이죠. 구현의 관점에서 이는 자동으로 메모화하는 것을 의미하지만, 반응성 프레이밍이 리액트와 Forget을 이해하는 더 좋은 방법이라고 생각합니다. 이에 대해 생각할 수 있는 하나의 방법은 현재 리액트가 객체의 식별자가 변경될 때 리렌더링 한다는 것입니다. Forget을 사용하면, 깊은 비교를 하는 데 런타임 비용을 발생시키지 않으면서 리액트는 값이 유의미하게 변경될 때만 리렌더링 합니다.
구체적인 진행 상황을 살펴보면, 지난 업데이트 이후 자동 반응성 접근 방식에 맞추고 내부적으로 컴파일러를 사용하면서 얻은 피드백을 반영하고자 컴파일러 설계를 여러 번 반복했습니다. 작년 말부터 컴파일러에 대한 몇 가지 중요한 리팩터링 작업을 거친 후, 이제 Meta 일부 영역의 프로덕션 환경에서 이 컴파일러를 사용하기 시작했습니다. 프로덕션 환경에서의 검증이 완료되면 React Forget을 오픈소스로 공개하고자 합니다.
마지막으로, 많은 분이 컴파일러의 작동 방식에 대해 많은 관심을 보여주셨는데요. 컴파일러를 검증한 후 오픈소스로 공개할 때 더 많은 자세한 내용을 공유하기를 기대하고 있습니다. 하지만 지금 공유할 수 있는 부분이 몇 가지 있습니다.
컴파일러의 코어는 바벨과 거의 완전히 분리되어 있으며, 코어 컴파일러 API는 (대략적으로) 오래된 AST 입력을 새로운 AST로 출력(소스 위치 데이터를 유지하면서)합니다. 내부를 들여다보면, 저수준의 의미 분석을 수행하기 위해 커스텀 코드 표현과 변환 파이프라인을 사용합니다. 그러나 컴파일러에 대한 기본적인 공개 인터페이스는 바벨과 다른 빌드 시스템 플러그인을 통해 이루어집니다. 테스트의 용이성을 위해 우리는 현재 각 함수의 새로운 버전을 생성하고 교체하기 위해 컴파일러를 호출하는 아주 얇은 래퍼로서의 바벨 플러그인을 사용하고 있습니다.
지난 몇 달 동안 컴파일러를 리팩터링 하면서 조건문, 반복문, 재할당, 뮤테이션과 같은 복잡한 기능을 처리할 수 있도록 핵심 컴파일 모델을 개선하는 데 집중하고자 했습니다. 하지만 자바스크립트에는 if/else, 삼항 연산, for, for-in, for-of 등 각 기능들을 표현하는 데 다양한 방법이 존재합니다. 처음부터 전체 언어를 지원하려고 했다면 핵심 모델을 검증하는 시점이 늦어졌을 겁니다. 대신에 let/const, if/else, for 반복문, 객체, 배열, 원시값, 함수 호출 등 기타 몇 가지 기능과 같이 작지만 대표적인 자바스크립트의 하위 집합부터 시작했습니다. 핵심 모델에 대한 자신감을 얻고 내부 추상화를 개선함에 따라, 지원되는 언어 하위 집합을 확장했습니다. 또한 아직 지원하지 않는 구문에 대해서는 명시적으로 로깅 진단을 수행하고 지원하지 않는 입력값에 대해서는 컴파일을 건너뛰고 있습니다. Meta의 코드 베이스에서 컴파일러를 사용해보고 지원되지 않는 기능 중 어떤 것이 가장 일반적인 기능인지 확인할 수 있는 유틸리티가 있습니다. 이를 통해 다음에 우선으로 처리해야 하는 기능을 우선순위에 맞게 정할 수 있습니다. 계속해서 점진적으로 전체 언어를 지원하도록 확장해 나갈 것입니다.
리액트 컴포넌트에서 일반 자바스크립트를 반응형으로 만들기 위해서는, 코드가 정확히 무엇을 하는지 깊이 이해하는 컴파일러가 필요합니다. 이러한 방식을 적용하여, 우리는 자바스크립트 내에, 도메인에 특화된 언어에 국한되지 않고 언어의 모든 표현 기능을 활용하여 복잡한 제품 코드를 작성할 수 있는 반응성 시스템을 생성하고 있습니다.
오프스크린 렌더링은 추가적인 성능 부담 없이 백그라운드에서 화면을 렌더링하기 위한 리액트의 최신 기능 중 하나입니다. DOM 요소뿐만 아니라 리액트 컴포넌트에서도 작동하는 content-visiblity
CSS 속성의 한 버전으로 이해하시면 됩니다. 연구 과정에서 다양한 사례를 발견했습니다.
대부분의 리액트 개발자는 리액트의 오프스크린 API와 직접 상호작용하지는 않을 것입니다. 대신 오프스크린 렌더링은 라우터와 UI 라이브러리 같은 곳에 통합되므로, 라이브러리를 사용하는 개발자는 추가적인 작업 없이 자동으로 이러한 이점을 누리게 됩니다.
중요한 것은 컴포넌트를 작성하는 방식을 변경하지 않고도 화면 밖에서 리액트 트리를 렌더링할 수 있어야 한다는 것입니다. 컴포넌트가 백그라운드에서 렌더링 되면, 컴포넌트가 보이기 전까지는 실제로 마운트되지 않으며 관련 이펙트가 실행되지 않습니다. 예를 들어, 한 컴포넌트가 처음 화면에 보일 때 useEffect
를 사용하여 분석을 로깅 한다고 할 때, 컴포넌트가 미리 렌더링 되었다고 해서 이 분석의 정확성을 망치지 않는다는 것입니다. 마찬가지로 컴포넌트가 화면에서 사라질 때 컴포넌트의 이펙트도 언마운트 됩니다. 오프스크린 렌더링의 핵심적인 기능은 컴포넌트의 가시성을 전환하면서도 컴포넌트의 상태를 유지할 수 있다는 점입니다.
지난 업데이트 이후, Meta 내부에서 안드로이드와 iOS 리액트 네이티브 애플리케이션에 프리렌더링의 실험 버전을 테스트했고 긍정적인 성능 결과를 보여주었습니다. 또한 오프스크린 렌더링이 서스펜스와 함께 동작하는 방식도 개선하여 오프스크린 트리 내에서 일시적인 중단이 발생해도 서스펜스 폴백이 발생되지 않도록 수정했습니다. 앞으로 남은 작업은 라이브러리 개발자에게 노출되는 기초 기능을 마무리하는 것입니다. 올해 말 테스트와 피드백을 위해 실험적인 API와 함께 RFC를 발표할 예정입니다.
트랜지션 추적 API는 리액트 트랜지션이 느려지는 시점을 감지하고 이에 대한 원인을 조사합니다. 지난 업데이트 이후, API의 초기 설계 작업을 완료했고 RFC도 공개했습니다. 기본적인 기능들 또한 구현되었습니다. 이 프로젝트는 현재 보류 중입니다. RFC에 대한 피드백은 적극 환영하며, 리액트의 더 나은 성능 측정 도구를 제공하기 위해 다시 개발을 재개하기를 기대합니다. 이 API는 Nest.js App Router와 같이 리액트 트랜지션 위에 구축된 라우터에서 특히 유용합니다.
이번 업데이트에 더해, 우리 팀은 최근 커뮤니티 팟캐스트와 라이브 스트리밍에 게스트로 출연하여 우리가 하는 작업에 대해 더 자세히 이야기하고 질문에 답변했습니다.
이 글을 리뷰해준 Andrew Clark, Dan Abramov, Dave McCabe, Luna Wei, Matt Carroll, Sean Keegan, Sebastian Silbermann, Seth Webster, Sophie Alpert에게 감사의 말씀을 전합니다.
읽어주셔서 감사합니다. 다음 업데이트에서 또 봬요!