태그
목차

React Labs 2023년 3월 작업중인 사항

생성일: 2024-02-01

수정일: 2024-02-01

React Labs 게시물에서는 현재 연구 개발 중인 프로젝트에 대한 글을 작성한. 지난 업데이트 이후 상당한 진전이 있었으며, 그 내용을 공유하고자 한다.

React 서버 컴포넌트

React Server Components (또는 RSC)는 React 팀에서 설계한 새로운 애플리케이션 아키텍처다.

우리는 RSC에 관한 연구를 처음으로 소개하는 강연과 RFC를 공유했다. 이를 요약하면 빌드 전에 실행되어 JavaScript 번들에서 제외되는 새로운 종류의 컴포넌트인 서버 컴포넌트(Server Components)를 소개한다. 서버 컴포넌트는 빌드 중에 실행될 수 있어 파일 시스템에서 읽거나 정적 콘텐츠를 가져올 수 있다. 또한 서버에서 실행될 수 있어 API를 구축할 필요 없이 데이터 레이어에 액세스할 수 있다. 브라우저에서 서버 컴포넌트에서 대화형 클라이언트 컴포넌트로 데이터를 전달할 수 있다.

RSC는 서버 중심의 Multi-Page 애플리케이션의 "요청/응답" 모델을 클라이언트 중심의 SPA(Single-Page Apps)의 매끄러운 상호 작용과 결합하여 양쪽의 장점을 제공한다.

지난 업데이트 이후에는 React Server Components RFC를 합의하여 제안을 승인했다. React Server Module Conventions 제안의 미해결된 문제를 해결하고 "use client" 규칙을 따르기로 파트너들과 합의에 도달했다. 이러한 문서들은 RSC 호환 구현체를 지원하기 위한 사양으로 작용한다.

가장 큰 변화는 서버 컴포넌트에서 데이터를 가져오는 기본 방법으로 async/await 방식을 도입했다는 점이다. 또한 프로미스를 언래핑하는 use 라는 새로운 Hook을 도입하여 클라이언트에서 데이터 로딩을 지원할 계획이다. 클라이언트 전용 앱의 임의 컴포넌트에서 async / await 을 지원할 수는 없지만, RSC 앱의 구조와 유사하게 클라이언트 전용 앱을 구조화할 때 이를 지원할 수 있도록 추가할 계획이다.

이제 데이터 불러오기는 꽤 잘 정리되었으므로 다른 방향, 즉 데이터베이스 뮤테이션을 실행하고 폼을 구현할 수 있도록 클라이언트에서 서버로 데이터를 전송하는 방법을 살펴보고 있다. 이를 위해 서버/클라이언트 경계를 넘어 서버 액션 함수를 전달하면 클라이언트가 이를 호출하여 원활한 RPC를 제공할 수 있다. 또한 서버 액션은 자바스크립트가 로드되기 전에 점진적으로 개선된 폼을 제공한다.

서버 컴포넌트는 Next.js App Router에 포함되어 출시되었다. 이는 RSC를 기본적으로 받아들이는 라우터의 심층적인 통합을 보여준다. 그러나 RSC 호환 라우터와 프레임워크를 구축하는 유일한 방법은 아니다. RSC 사양과 구현에서 제공하는 기능은 명확하게 구분되어 있다. 서버 컴포넌트는 호환되는 React 프레임워크에서 작동하는 컴포넌트를 위한 명세로서 의도되었다.

일반적으로 기존 프레임워크를 사용하는 것이 좋지만 사용자 정의 프레임워크를 구축해야 하는 경우 가능하다. 자체 RSC 호환 프레임워크를 구축하는 것은 현재로서는 생각만큼 쉽지 않다. 주로 깊은 번들러 통합이 필요하기 때문이다. 현재의 번들러 세대는 클라이언트에서 사용하기에 훌륭하지만 서버와 클라이언트 간에 단일 모듈 그래프를 분할하는 데 대한 일급 지원이 없다. 이것이 우리가 지금 직접 번들러 개발자와 협력하여 RSC의 기본 요소를 구축하는 이유다.

에셋 로딩

Suspense 를 사용하면 컴포넌트의 데이터나 코드가 로드되는 동안 화면에 표시할 내용을 지정할 수 있다. 이를 통해 사용자는 페이지가 로드되는 동안은 물론 더 많은 데이터와 코드를 로드하는 라우터 탐색 중에도 점진적으로 더 많은 콘텐츠를 볼 수 있다. 하지만 사용자 입장에서는 새 콘텐츠가 준비되었는지 여부를 고려할 때 데이터 로딩과 렌더링만으로는 어떤 상황인지 알 수 없다. 기본적으로 브라우저는 스타일시트, 글꼴, 이미지를 독립적으로 로드하므로 UI 점프와 연속적인 레이아웃 시프트가 발생할 수 있다.

우리는 Suspense 를 스타일시트, 글꼴 및 이미지의 로딩 라이프사이클과 완전히 통합하여 React가 이를 고려하여 내용이 표시 준비가 되었는지를 결정하도록 작업하고 있다. React 컴포넌트를 작성하는 방식에 대한 어떠한 변경도 필요하지 않으며, 업데이트는 더 일관되고 만족스러운 방식으로 작동할 것이다. 최적화로서 우리는 컴포넌트에서 직접 글꼴과 같은 에셋을 사전로드할 수 있는 수동 방법도 제공할 것이다.

현재 이러한 기능을 구현 중이며, 곧 더 많은 내용을 공유할 예정이다.

문서 메타데이터

앱의 페이지와 화면마다 <title> 태그, 설명 및 이 화면과 관련된 기타 <meta> 태그와 같은 메타데이터가 다를 수 있다. 유지 관리 관점에서 이 정보를 해당 페이지나 화면의 React 컴포넌트에 가깝게 유지하는 것이 확장성이 더 높다. 하지만 이 메타데이터에 대한 HTML 태그는 일반적으로 앱의 최상위 컴포넌트에서 렌더링되는 <head> 문서에 있어야 한다.

오늘날 사람들은 두 가지 기술 중 하나를 사용하여 이 문제를 해결한다.

한 가지 기법은 특별한 서드파티 컴포넌트를 렌더링하여 그 안에 있는 <title>, <meta> 및 기타 태그를 문서 <head> 로 이동시키는 것이다. 이 방법은 주요 브라우저에서 작동하지만 Open Graph Parser와 같이 클라이언트 측 자바스크립트를 실행하지 않는 클라이언트가 많기 때문에 이 기술이 보편적으로 적합하지는 않다.

다른 하나의 기술은 페이지를 두 부분으로 서버 렌더링하는 것이다. 먼저, 주요 콘텐츠가 렌더링되고 모든 해당 태그가 수집된다. 그런 다음 이러한 태그를 사용하여 <head> 가 렌더링된다. 마지막으로 <head> 와 주요 콘텐츠가 브라우저로 전송된다. 이 접근 방식은 작동하지만 React 18의 스트리밍 서버 렌더러의 이점을 활용할 수 없게 되며 <head> 를 전송하기 전에 모든 콘텐츠가 렌더링되기를 기다려야 한다.

그래서 컴포넌트 트리의 어느 곳에서나 <title> , <meta> , 메타데이터 <link> 태그를 렌더링할 수 있도록 기본 지원을 추가했다. 이 기능은 완전한 클라이언트 측 코드, SSR, 그리고 향후 RSC를 포함한 모든 환경에서 동일한 방식으로 작동할 것이다. 이에 대한 자세한 내용은 곧 공유할 예정이다.

React 최적화 컴파일러

이전 업데이트 이후로 React Forget에 대한 설계를 적극적으로 반복하고 있는데, 이는 React를 위한 최적화 컴파일러다. 우리는 이것을 "auto-memoizing compiler"로 이야기했었는데, 이것은 어떤 면에서는 사실이다. 그러나 이 컴파일러를 구축하는 것은 우리에게 React의 프로그래밍 모델을 더 깊게 이해하게 도와주었다. React Forget을 "automatic reactivity compiler"로 이해하는 것이 더 나은 방법이라고 생각한다.

React의 핵심 아이디어는 개발자가 UI를 현재 상태의 함수로 정의한다는 것이다. 개발자는 숫자, 문자열, 배열, 객체와 같은 일반적인 JavaScript 값과 if/else, for 등과 같은 표준 JavaScript 관용구를 사용하여 컴포넌트 로직을 설명한다. 멘탈 모델은 애플리케이션 상태가 변경될 때마다 React가 다시 렌더링한다는 것이다. 이 단순한 멘탈 모델과 코드를 자바스크립트 시맨틱에 가깝게 유지하는 것이 React의 프로그래밍 모델에서 중요한 원칙이다.

그러나 문제는 React가 때로는 너무 반응적일 수 있다는 것이다. 예를 들어 JavaScript에서는 두 객체나 배열이 동일한지 비교하는 저렴한 방법이 없기 때문에 각 렌더링마다 새로운 객체나 배열을 만들면 React가 필요 이상으로 작업을 수행할 수 있다. 이는 개발자가 변경 사항에 과도하게 반응하지 않도록 명시적으로 컴포넌트를 메모이즈해야 하는 경우가 있음을 의미한다.

React Forget의 목표는 상태 값이 의미 있게 변경될 때만 앱이 다시 렌더링되도록 하여 React 앱이 기본적으로 적절한 양의 반응성을 갖도록 하는 것이다. 구현 관점에서 볼 때 이는 자동 메모화를 의미하지만, 우리는 reactivity framing이 React와 Forget을 이해하는 더 좋은 방법이라고 믿는다. 이에 대해 생각해 볼 수 있는 한 가지 방법은 현재 React는 객체 ID가 변경되면 다시 렌더링한다는 것이다. Forget을 사용하면 React는 값이 의미있게 변경될 때 다시 렌더링하지만 심층 비교로 인한 런타임 비용이 발생하지 않는다.

구체적인 진행 상황을 공유해보자면, 지난 업데이트 이후 automatic reactivity 접근 방식에 맞춰 컴파일러를 설계하고 내부적으로 컴파일러를 사용하면서 얻은 피드백을 반영하기 위해 컴파일러 설계를 상당히 반복적으로 개선했다. 작년 말부터 컴파일러를 대대적으로 리팩터링한 결과, 이제 메타의 제한된 영역에서 이 컴파일러를 프로덕션에 사용하기 시작했다. 프로덕션 환경에서 성능이 입증되면 오픈소스로 공개할 계획이다.

마지막으로, 많은 분들이 컴파일러의 작동 방식에 관심을 표명해 주었다. 컴파일러를 검증하고 오픈소스로 공개할 때 더 많은 세부 정보를 공유할 수 있기를 기대한다. 하지만 지금 공유할 수 있는 부분은 몇 가지 있다:

컴파일러의 코어는 Babel에서 거의 완전히 분리되어 있으며, 코어 컴파일러 API는 (대략적으로) 이전 AST를 입력하고 새 AST를 출력하는 방식이다(소스 위치 데이터는 유지하면서). 내부적으로는 저수준 의미 분석을 수행하기 위해 사용자 정의 코드 표현 및 변환 파이프라인을 사용한다. 그러나 컴파일러에 대한 기본 공개 인터페이스는 Babel 및 기타 빌드 시스템 플러그인을 통해 이루어진다. 테스트의 편의를 위해 현재 각 함수의 새 버전을 생성하고 이를 교체하기 위해 컴파일러를 호출하는 매우 얇은 래퍼인 Babel 플러그인이 있다.

지난 몇 달 동안 컴파일러를 리팩토링하는 과정에서 우리는 핵심 컴파일 모델을 개선하여 조건문, 반복문, 재할당 및 뮤테이션과 같은 복잡성을 처리할 수 있도록 중점을 두고자 했다. 그러나 JavaScript에는 각각의 기능을 표현하는 다양한 방법이 있습니다: if/else, 삼항 연산자, for, for-in, for-of 등등. 처음부터 전체 언어를 지원하려고 하면 핵심 모델을 검증할 수 있는 시점을 지연시켰을 것이다. 대신, 우리는 언어의 대표적인 하위 집합에서 시작했다. let/const, if/else, for 루프, 객체, 배열, 프리미티브, 함수 호출 및 몇 가지 다른 기능이 포함된 대표적인 하위 집합이었다. 핵심 모델에 대한 확신을 얻고 내부 추상화를 정립함에 따라 지원되는 언어 하위 집합을 확장했다. 우리는 아직 지원하지 않는 구문에 대해 진단을 기록하고 지원되지 않는 입력에 대해 컴파일을 건너뛰는 등의 작업을 수행한다. 메타의 코드베이스에서 컴파일러를 시험해보고 가장 많이 지원되지 않는 기능을 파악하여 다음 지원 우선순위를 정할 수 있는 유틸리티가 있다. 전체 언어를 지원하는 방향으로 점진적으로 확장해 나갈 것이다.

React 컴포넌트에서 일반 JavaScript를 반응형으로 만들려면 코드가 정확히 무엇을 하는지를 정확히 이해할 수 있는 깊은 의미론적 이해가 있는 컴파일러가 필요하다. 이러한 접근 방식을 통해 JavaScript 내에서 반응성을 위한 시스템을 만들고, 언어의 전체 표현력을 활용하여 어떠한 복잡도의 제품 코드도 작성할 수 있게 된다. 이는 도메인 특화 언어에 제한되지 않고 언어의 전체 표현력을 활용할 수 있는 시스템을 생성하게 된다.

오프스크린 렌더링

오프스크린 렌더링은 추가적인 성능 오버헤드 없이 백그라운드에서 화면을 렌더링하기 위한 기능이다. 이는 DOM 요소뿐만 아니라 React 컴포넌트에도 작동하는 content-visibility CSS 속성의 버전으로 생각할 수 있다. 연구 중에 우리는 다양한 사용 사례를 발견했다:

대부분의 React 개발자는 React의 오프스크린 API와 직접 상호작용하지 않는다. 대신, 오프스크린 렌더링은 라우터와 UI 라이브러리 등에 통합되어 해당 라이브러리를 사용하는 개발자는 추가 작업 없이 자동으로 이점을 누릴 수 있다.

컴포넌트를 작성하는 방식을 변경하지 않고도 오프스크린(화면 밖)에서 React 트리를 렌더링할 수 있어야 한다는 것이다. 컴포넌트가 오프스크린에서 렌더링되면 컴포넌트가 표시될 때까지 실제로 마운트되지 않으며, 그 Effect는 실행되지 않는다. 예를 들어, 컴포넌트가 처음 표시될 때 useEffect 를 사용하여 분석을 기록하는 경우 미리 렌더링해도 해당 분석의 정확성이 손상되지 않는다. 마찬가지로 컴포넌트가 화면 밖으로 나가면 해당 Effect도 언마운트 된다. 오프스크린 렌더링의 핵심 기능은 컴포넌트의 상태를 그대로 유지하면서 표시 여부를 전환할 수 있다는 것이다.

지난 업데이트 이후로 Meta에서는 React Native 앱에서 Android 및 iOS에서 prerendering의 실험 버전을 테스트하고 양호한 성능 결과를 얻었다. 또한 오프스크린 렌더링이 Suspense와 함께 작동하는 방식도 개선했다. 오프스크린 트리 내에서 중단이 발생하면 Suspense 폴백이 트리거되지 않는다. 남은 작업은 라이브러리 개발자에게 노출되는 기본 요소들을 최종화하는 것이다. 우리는 올해 말에 RFC를 게시할 예정이며, 실험적인 API도 함께 제공하여 테스트하고 피드백을 받을 것이다.

트랜지션 추적

트랜지션 추적(Transition Tracing) API를 사용하면 React 트랜지션이 느려지는 시점을 감지하고 느려지는 이유를 조사할 수 있다. 지난 업데이트 이후 API의 초기 설계를 완료하고 RFC를 게시했다. 기본 기능도 구현되었다. 이 프로젝트는 현재 보류 중이다. RFC에 대한 피드백을 환영하며, 더 나은 React 성능 측정 도구를 제공하기 위해 개발을 재개할 수 있기를 기대한다. 이 기능은 Next.js 앱 라우터와 같이 React 트랜지션 위에 구축된 라우터에 특히 유용할 것이다.