
원문: Common React libraries architecture
리액트는 풍부한 생태계로 유명합니다. 리액트 생태계에는 상태 관리, 폼, 라우팅, 스타일 등의 다양한 도구들이 있는데 이들은 내부적으로 어떻게 동작할까요?
대부분의 도구는 일부 아키텍처 선택에서 유사하며, 리액트의 작동 방식에 더 부합합니다. 이러한 일반적인 구조를 살펴보겠습니다.
바인딩
대부분의 라이브러리는 코어와 바인딩이라는 두 가지 주요 부분으로 구성된 아키텍처에 의존합니다. 코어는 로직과 기능이 있는 곳이고 바인딩은 코어와 프런트엔드 도구 사이의 연결입니다. 리액트의 경우, 프런트엔드 도구는 컴포넌트와 커스텀 훅을 의미합니다.
대부분의 경우 코어 객체는 외부에서 생성됩니다. 이렇게 하면 리렌더링 사이클에서 벗어나고, 리액트 메모이제이션 과정에 신경 쓸 필요가 없는 등의 여러 이점이 있습니다. 이를 리액트와 연결하기 위해 일반적으로 Context API에 의존합니다.
그렇기 때문에 많은 도구에 Provider가 있습니다. 이는 라이브러리가 모든 컴포넌트 트리에 외부 코어 데이터를 주입하는 방식입니다. Context API는 동적 데이터에 문제가 있어 성능 문제가 발생할 수 있지만 코어의 참조가 안정적이기 때문에 문제가 되지 않습니다.
Context API를 적용하면 외부 데이터를 직접 연결하지 않더라도, 리액트 트리의 Provider 아래에 다른 컴포넌트와 훅을 추가하는 것만으로 외부 데이터에 접근할 수 있습니다. Tanstack Query의 경우를 예로 들어보겠습니다. 코어 클라이언트와 Provider를 사용하면 서로 다른 페이지에서 동일한 쿼리 캐시를 사용할 수 있으며, 모두 동일한 키를 사용하기만 하면 됩니다. 라이브러리는 코어 및 Context Provider 전체에서 모든 것을 연결하므로 이 데이터를 공유하는 데 신경 쓸 필요가 없습니다.
이같은 방식은 React-router와 같은 라이브러리에서도 볼 수 있습니다. 당신이 useParams, useNavigate를 호출하여 데이터나 메서드를 가져오고자 하면, React-router는 커스텀 훅에서 내부적으로 사용하는 컨텍스트를 통해 이를 제공합니다.
외부 연결
하지만 또 다른 문제가 있습니다. 클라이언트는 리액트 렌더링 모델과 연결하기 위해 반드시 어떤 패턴을 사용해야 합니다. 클라이언트에서 무언가가 변경되었을 때, 리액트가 최신 값을 사용하기 위해 다시 렌더링 해야 한다는 것을 어떻게 알 수 있을까요?
대부분은 옵서버 패턴을 사용합니다. 여기에는 데이터 변경 사항을 알리기 위해 구독하고 콜백 함수를 실행하는 메서드가 있습니다. 그리고 현재 원시(raw) 데이터를 가져오는 getState 메서드와, 상태 업데이트를 트리거 하는 메서드(상태 설정자, 디스패치, 리페치 등 무엇이든)가 있습니다. 물론 라이브러리의 코어에는 더 많은 메서드가 포함될 수 있지만, 이 세 가지(구독 및 알림 메서드, getState 메서드, 상태 업데이트 트리거 메서드)는 필수적입니다.
리액트와 연결하기 위해서는 2가지 방식이 있습니다.
- useSyncExternalStore: 외부 데이터를 다루기 위한 리액트 기본 훅입니다. 이 훅에 getState 및 subscribe 함수(추가로 서버 측 렌더링이 필요한 경우에는 이를 위한 getState 포함)를 전달하면, 훅은 변경 사항을 구독하고, 데이터 변경 시 다시 렌더링 하여 getState 함수를 호출함으로써 변경된 데이터를 반환합니다.
- 커스텀 훅: useEffect와 useReducer를 이용하여 자신만의 버전을 만들 수 있습니다. useSyncExternalStore 훅이 존재하기 이전에는 모든 사람이 이 방법을 사용했고, 이는 나름의 이점이 있었습니다.
useSyncExternalStore는 최적화가 되어있지 않습니다. 그래서 우선순위가 높은 업데이트로 취급되고, 애플리케이션에 동시성 기능이 있는 경우 그에 대한 최적화를 무효화합니다. 이렇게 하면 테어링(또는 사용 중인 상태의 업데이트되지 않은 버전)을 방지할 수 있으므로, 바람직한 동작일 수 있습니다.
동시성 기능은 기본적으로 다른 상태와의 우선 순위를 정하기 위해 일부 상태의 테어링을 허용하는 것은 사실입니다. 하지만 리액트는 외부 상태의 "테어링"을 나쁜 패턴으로 간주하기 때문에, 클라이언트가 알림을 트리거하면 리액트는 이를 동기화하고, 이에 따라 최신 버전의 데이터로 리렌더링이 발생합니다.
자체 연결을 생성하여 외부 상태를 우선순위가 가장 낮은 상태로 취급하고, 다른 상호작용을 처리하기 위해 마지막 업데이트의 재렌더링을 일시 중지한다면, 이 시나리오를 피할 수 있습니다.
이는 장단점을 따져봐야 하는 문제이지만 대부분의 라이브러리는 useSyncExternalStore을 사용하는 것을 고수하고 있습니다.
리액트 19
리액트 19에는 라이브러리 구현에 영향을 미칠 업데이트가 있습니다. 대부분 서스펜스와 프로미스를 처리하기 위해 use 훅을 사용하겠지만, 새로운 훅이 더 필요한 다른 사례도 있을 것입니다.
예를 들어, 폼 라이브러리는 특히 Next.js, Waku 및 Tanstack Start와 같은 리액트 서버 컴포넌트에서 useOptimistic, useFormStatus 및 useActionState 를 매우 유용하게 사용할 수 있습니다.
이러한 아키텍처들은 특정한 사례에 맞춰 발전하고 변화할 수 있지만, 일반적으로는 이 글에서 그린 정도가 현재의 리액트 생태계 구조에 대한 전체적인 그림입니다.
🚀 한국어로 된 프런트엔드 아티클을 빠르게 받아보고 싶다면 Korean FE Article을 구독해주세요!
'개발 > 번역' 카테고리의 다른 글
| [번역] CSS 길이 단위 이해하기 (1) | 2025.09.02 |
|---|---|
| [번역] React.memo 완벽 해부: 언제 쓸모 있고 언제 쓸모없는가 (10) | 2025.08.11 |
| [번역] 리액트의 개방-폐쇄 원칙: 확장 가능한 컴포넌트 만들기 (3) | 2025.06.29 |
| [번역] 클린 코드의 심리학: 우리가 지저분한 리액트 컴포넌트를 작성하는 이유 (0) | 2025.06.21 |
| [번역] 리액트에서의 의존성 역전: 테스트하기 쉬운 컴포넌트 만들기 (0) | 2025.05.25 |