
React의 역사: 0.4부터 19.2까지, 동시성과 패러다임의 위대한 타임라인
최근에 새로운 프로젝트를 시작하면서 Next.js 16과 React 19.2라는 아주 강력한 신무기들을 장착하고 달리려다 보니, 그동안 파편적으로만 알고 있거나 제대로 정리하지 못한 React의 거대한 변화 흐름들이 계속 마음에 걸렸습니다.
실제로 최근 도입된 혁신적인 API들이나 설계 패턴들에 대해 질문을 해보면 눈부시게 발전한 AI조차 맥락을 제대로 추천하지 못하거나, 오히려 잘못된 방향으로 코드를 지적하는 일도 빈번하게 겪었습니다. 저는 **"결국 이 시대의 프론트엔드 생태계에서 단단하게 살아남으려면, 그 기술이 탄생하게 된 기존의 철학과 역사적 배경을 깊이 이해해야만 한다"**고 생각했습니다.
그래서 이번 기회에 시간을 내어 React 공식 문서(Document)와 React Core 팀의 블로그 질의응답들을 집요하게 파고들며 그들이 과거부터 어떤 방향성으로 깊은 고민을 해왔는지 공부하고 정리했습니다.
이 글은 과거 React 0.4 시절부터 어쩌면 완성형에 도달한 최근 19.2 버전까지의 굵직한 타임라인과 역사를 다룹니다. 우연히 이 글을 스쳐 지나가는 동료 개발자분들께 작은 통찰과 도움이 되었으면 좋겠고, 무엇보다 훗날 미래의 저 자신에게 방향을 잃었을 때 다시 한번 든든하게 꺼내 읽을 수 있는 이정표 같은 글이 되기를 바랍니다.
2013 - 2014년: 이단아의 첫 출발 (v0.3 ~ v0.14)
시대적 배경: AngularJS의 천하, 그리고 Vue의 태동
이 시기 웹 개발의 제왕은 단연코 구글의 AngularJS 였습니다. 강력한 양방향 데이터 바인딩 덕분에 "모델이 바뀌면 뷰가 바뀌고, 뷰가 바뀌면 모델이 바뀐다"는 엄청난 편리함을 제공했습니다. 한편으로는 이 복잡성을 덜어내기 위해 조금 더 가볍고 세련된 접근을 시도한 Vue.js가 은밀하게 모습을 드러내던 타이밍이기도 합니다.
이런 상황에서 Facebook(현 Meta)이 오픈소스로 막 공개하며 모습을 드러내기 시작한 초창기 React(특히 v0.4 근방)는 기존 문법을 파괴하는 완벽한 '이단아' 였습니다.
- "HTML과 JS를 분리하지 않고 JSX라는 이상한 문법으로 섞어 쓴다."
- "데이터가 변하면 그냥 전체 화면을 날려버리고 다시 렌더링한다."
당시 개발자들 사이에서는 "관심사 분리(SoC)를 역행하는 미친 짓이다!", "이게 과연 효율적인가?"라는 비판이 거셌습니다. 또한, 당시 v0.4 시절의 React는 클래스 생명주기 간의 코드를 재사용하기 위해 **Mixins(믹스인)**이라는 개념을 적극적으로 도입했었습니다. (물론 이 Mixin들은 훗날 수많은 충돌과 네이밍 사이드 이펙트를 일으키며 'HOC'로, 그리고 결국 'Hooks'로 대체되는 뼈아픈 역사의 시작점이 되기도 합니다.)
엄청난 비난에도 불구하고 React 팀은 흔들리지 않았습니다. 그들은 Virtual DOM(가상 돔) 이라는 무기를 통해, 전체를 다시 렌더링하는 추상화를 유지하면서도 브라우저 성능을 최적화하는 기적을 보여주었습니다. 이것이 바로 단방향 데이터 흐름(One-way Data Flow) 에 기반한 선언적 UI의 위대한 시작이었습니다.
2016년: 버전 체계의 정립과 뷰 엔진의 정돈, React 15
0.14 버전 다음, 갑자기 1.0이 아닌 React 15가 등장했습니다. 이는 "이미 현업 생태계에서 매우 광범위하고 안정적으로 쓰이고 있는데도 앞자리가 0인 것은 합당하지 않다"며, 메이저 버전(SemVer) 프레임워크로 공식 선언하기 위한 React 팀의 자신감이었습니다. (기존 0.14 내부 버전을 14로 치고 15로 점프했습니다.)
React 15에서 가장 눈에 띄는 변화는 지저분한 DOM 찌꺼기의 제거였습니다.
과거 0.x 버전 시절에는 React가 렌더링한 모든 HTML 태그마다 < div data-reactid=".0.1.2"> 처럼 React 내부적으로 위치를 추적하기 위한 거대한 data-reactid 어트리뷰트가 강제로 붙었습니다. 이는 문서 크기를 불필요하게 키우고 DOM을 지저분하게 만들었습니다.
React 15에서는 자체적인 DOM 트리 관리 방식을 획기적으로 개선하여, 이런 어트리뷰트들을 전부 제거하고 우리가 작성한 깨끗한 순수 DOM 요소만을 렌더링하게 되었습니다. 또한 SVG 지원이 대폭 강화되면서 React를 통해 수려한 그래프나 인터랙티브한 시각 데이터를 그릴 수 있는 훌륭한 환경이 안정적으로 다져진 중요한 시기였습니다.
2017년: 혁명의 불씨, React 16과 "Fiber Architecture"
React가 시장 점유율을 공격적으로 흡수하며 덩치를 키우던 중, 피할 수 없는 한계에 부딪혔습니다. 싱글 스레드 기반인 브라우저에서 무거운 컴포넌트 트리를 렌더링할 때 화면이 버벅이는 현상(Blocking)이 발생한 것입니다.
이를 해결하기 위해 React 코어 팀은 프레임워크 내부 깊은 곳의 렌더링 엔진을 바닥부터 완전히 새로 짰습니다. 이 위대한 재설계의 산물이 바로 React Fiber(파이버) 아키텍처입니다.
이러한 중단과 재개가 가능해진 결정적인 이유는 React가 렌더링 과정을 **Render Phase(렌더 단계)**와 Commit Phase(커밋 단계) 두 가지로 분리했기 때문입니다.
- Render Phase: 컴포넌트를 호출해 가상 돔(Virtual DOM)을 만들고 이전과 비교(Diffing)하는 작업입니다. Fiber 아키텍처 덕분에 이 단계의 작업들은 중단되거나 폐기, 또는 우선순위가 뒤로 밀릴 수(Asynchronous) 있게 되었습니다.
- Commit Phase: 앞서 계산된 변경 사항을 실제 브라우저 DOM에 단번에 적용하고 화면에 그리는(Painting) 단계입니다. 이 과정은 중단 없이 **동기적(Synchronous)**으로 진행되어 사용자에게 불완전한 UI(Tearing, 화면 찢어짐)가 노출되는 것을 방지합니다.
여담: 이 시대 개발자들에게 너무나 익숙한 그 '삼각형 애니메이션'
추천 레퍼런스 영상: Lin Clark - A Cartoon Intro to Fiber (React Conf 2017)
Fiber 아키텍처가 처음 소개되었던 컨퍼런스 영상(React Conf 2017)을 기억하시는 분들이 많을 겁니다. 화면 한가득 수많은 작은 삼각형들이 움직이는 데모였습니다. 기존 엔진(Stack Reconciler)에서는 렌더링 계산이 오래 걸려 메인 스레드를 꽉 막아버리니 애니메이션이 버벅거리며 뚝뚝 끊겼습니다. 반면, 새로운 Fiber 엔진을 켠 화면에서는 그 무거운 렌더링 작업을 하면서도 끊김 없이 부드럽게 삼각형들이 돌아갔습니다. 이 놀라운 시각적 차이에 모든 프론트엔드 개발자들이 환호하며, 다가올 동시성 시대의 위력을 두 눈으로 실감했던 명장면이었습니다.

stack

fiber
역사적으로 아주 흥미로운 점은, 정작 React 16 버전에 Fiber 엔진이 성공적으로 도입되었음에도, 실제 개발자들이 쓸 수 있는 동시성 API(Concurrent Mode)는 이 시점에서 당장 개방되지 않았다는 사실입니다. 겉보기에는 이전 버전과 똑같이 동기적으로 동작(Sync Rendering)했지만, 이 거대한 '엔진 교체'는 훗날 React가 **동시성(Concurrency)**의 완전한 꿈(Suspense, Transitions)을 18버전과 19.2에서 이루기 위한 가장 중요하고 거대한 토대가 되었습니다.
2018년 후반: 엔진 교체의 첫 번째 성과, React 16.6 Suspense의 첫 등장
Fiber로 뼈대를 완전히 교체한 React 팀은 16.6 릴리즈를 통해 "왜 엔진을 무서운 리스크를 지면서 교체했는가?"에 대한 첫 번째 해답을 내놓았습니다. 바로 Suspense의 초기 모델과 React.lazy()의 등장이었습니다.
이전에는 번들에 포함된 거대한 코드 조각들을 사용자가 필요로 할 때 동적으로 쪼개서 가져오는 작업(Code Splitting) 스펙이 매우 복잡했습니다. 서드파티 라이브러리 없이는 로딩 상태를 직접 관리해야 했죠.
React 16.6에 등장한 Suspense는 lazy()로 불러오는 동적 컴포넌트들을 감싸 안으며, **"이 컴포넌트의 코드가 아직 다운로드되지 않았으면 렌더링을 멈추고 제어권을 나에게 줘, 그럼 내가 Fallback(로딩 스피너)을 띄워줄게"**라는 컨셉을 증명했습니다.
비록 이때의 Suspense는 오직 '코드 스플리팅'에만 한정되어 데이터를 패칭하는 비동기 통신에는 쓰이지 못했지만, 렌더링 과정을 일시적으로 멈추고(Pause) 위임할 수 있다는 Fiber 엔진의 능력을 실제 개발자들이 맛보게 한 역사적인 첫 API였습니다.
2019년: React 16.8 패러다임 시프트 (함수형 컴포넌트와 Hooks)
최고의 전환점이자 논쟁의 중심이 되었던 시기입니다. 클래스와 생명주기(Lifecycle) 메서드 중심의 개발 방식에서, 함수형 컴포넌트(Function Component)와 Hooks로 생태계 전체를 뒤집어버렸습니다.
React 팀이 이처럼 급진적인 변화를 선택한 3가지 결정적 이유가 있었습니다.
-
상태 로직 재사용의 한계와 Wrapper Hell: 로직을 재사용하기 위해 고차 컴포넌트(HOC)나 Render Props 패턴을 쓰다 보니, 컴포넌트 트리가 끝없이 깊어지는 지옥이 펼쳐졌습니다.
// 과거 HOC가 만들어낸 끔찍한 Wrapper Hell의 예시 <ReduxProvider> <ThemeProvider> <WithAuth> <WithRouter> <MyComponent /> </WithRouter> </WithAuth> </ThemeProvider> </ReduxProvider>Hooks는 상태 자체를 분리된 모듈처럼 꺼내어 재사용할 수 있게 해주어 이런 지옥을 단숨에 평정했습니다.
-
파편화된 클래스 생명주기(Lifecycle)와 동시성(Concurrency) 도입의 구조적 불가능성: 단순히 로직이 흩어져서 보기 지저분하다는 수준의 문제가 아니었습니다. 클래스형에서는 하나의 기능을 처리하기 위해 데이터 패칭과 이벤트 구독 등이
componentDidMount,componentDidUpdate,componentWillUnmount이리저리에 파편화되어 찢어졌습니다. 동시성 모드의 핵심은 렌더링 작업(Render Phase)을 중간에 수시로 멈추고(Pause), 취소하고, 다시 재개(Resume)하는 것입니다. 그런데 이렇게 상태 관련 로직들이 수많은 생명주기 메서드에 사방팔방 찢어진 클래스 구조에서는, 엔진이 렌더링을 멈추거나 취소하고 다시 계산하려 할 때 데이터의 불일치(Inconsistency)와 예측 불가능한 거대한 혼란(사이드 이펙트의 중복 실행 등)을 완벽하게 제어하는 것이 구조적으로 완전히 불가능했습니다. 반면, **함수형 컴포넌트와 Hooks(useEffect등)**는 연관된 로직들을 하나의 클로저 공간 안으로 아름답게 응집시켰고, 렌더링 자체를 그저 "상태를 인자로 받아 UI를 반환하는 단순한 수학적 호출"로 탈바꿈시켜, React 엔진이 언제든 자유롭고 안전하게 렌더링 턴을 멈추고 반복할 수 있는 동시성 시대의 토대가 되었습니다. -
클래스의
this복잡성과 컴파일 최적화 한계: JavaScript의this는 앞서 언급한 상태 불일치 버그를 유발하는 단골손님이었습니다. 또한 무거운 클래스 인스턴스 형태는 컴파일러가 코드를 정적 분석하고 압축(Minification)하는 데 한계가 컸던 반면, "단순한 함수"로 돌아간 컴포넌트는 오버헤드 없이 동시성 스케줄링을 훨씬 매끄럽게 수행할 수 있었습니다.
2020년: 발돋움을 위한 숨 고르기, React 17 (안정기)
React 17은 기능적으로 보면 놀라울 정도로 "아무것도 없는(No New Features)" 릴리즈로 유명합니다. 하지만 이는 다가올 동시성 폭풍을 견디기 위한 점진적 업그레이드 토대 마련이 핵심이었습니다. 여러 버전의 React를 하나의 페이지에 혼용할 수 있게 하고, 내부 이벤트 위임(Event Delegation) 방식을 Document에서 Root 컨테이너 레벨로 격하하여 버그를 줄였습니다. 폭풍 전야의 완벽한 기반 다지기였습니다.
2022년: 진정한 '동시성(Concurrency)' 엔진의 본격 가동, React 18
React 18은 16 시절 'Fiber'로 다져둔 동시성 엔진을 비로소 개발자들이 명시적으로 제어할 수 있게 끄집어낸 거대한 도약입니다.
그렇다면 동시성의 핵심이 무엇일까요? 앞서 React 16(Fiber)에서 언급했듯, React의 렌더링은 가상 돔을 비교하는 'Render Phase'와 실제 화면에 그리는 'Commit Phase'로 나뉩니다. 기존에는 이 Render Phase가 한번 시작되면 끝날 때까지 멈출 수 없었습니다. 하지만 이제 React는 Render Phase의 작업들을 잘게 쪼개고, **사용자의 클릭이나 타이핑 같은 긴급한 업데이트가 들어오면 기존의 무거운 렌더링을 즉시 중단(Pause)하고 긴급한 일부터 처리(Priority)**할 수 있게 되었습니다. 즉, **"작업의 우선순위(Priority)를 엔진 레벨에서 쥐락펴락하는 것"**이 동시성의 본질입니다.
그 중심에서 비동기 경험을 바꾼 것이 바로 Suspense 입니다.
과거에는 아래와 같이 if (isLoading) 분기 코드를 컴포넌트 내부에서 직접 도배하며 로딩 상태(우선순위)를 수동으로 제어했습니다.
// React 18 이전의 파편화된 데이터 패칭 방식
function MyProfile() {
const { data, isLoading } = useUser();
if (isLoading) return <Spinner />;
return <div>{data.name}</div>;
}
하지만 18버전부터는 **"이 데이터 패칭은 덜 중요하니까 준비될 때까지 렌더링 우선순위를 뒤로 미루고, 일단 빈 껍데기(Fallback UI)부터 먼저 그려서 사용자 인터랙션을 막지 마!"**라는 선언적인 스케줄링을 React 내부 엔진에 내릴 수 있게 된 것입니다.
// React 18 Suspense: 제어권을 엔진(상위)으로 넘긴다!
<Suspense fallback={<Spinner />}>
<MyProfile /> <!-- 내부 데이터가 올 때까지 렌더링 Pause -->
</Suspense>

참고: New Suspense SSR Architecture in React 18
결과적으로 앞으로 등장할 React의 거의 모든 새로운 메이저 API와 훅(Hooks)들은 전부 이 Render Phase의 '우선순위(Priority)'를 어떻게 더 섬세하게 조율할 것인가에 집중될 것입니다.
실제로 React 18 릴리즈와 함께, 위와 같은 렌더링 우선순위 제어 및 상태 동기화 문제를 해결하기 위한 강력한 무기(Hooks/API)들이 대거 등장했습니다.
📌 React 18이 쏟아낸 동시성 제어 무기들
useTransition/startTransition: "이 렌더링(예: 거대한 리스트 필터링)은 덜 급하니까, 사용자의 키보드 타이핑 등 긴급한 인터랙션을 절대 막지 말고 여유가 생길 때 백그라운드에서 처리해!"라고 상태 업데이트의 우선순위를 직접 낮추는 동시성의 핵심 무기입니다.useDeferredValue:useTransition처럼 상태 업데이트 함수가 아닌, '값(Value)' 자체의 렌더링을 지연(Defer)시켜 무거운 컴포넌트의 리렌더링 시 화면 버벅임 없이 우선순위를 미뤄줍니다.useSyncExternalStorage: 동시성 엔진이 렌더링을 잠시 멈춘(Pause) 사이에 Redux 등 외부 스토어의 데이터가 외부에서 변해버리는 사고(화면 일부만 구버전 데이터를 그리는 화면 찢어짐, Tearing 버그)를 완벽하게 방어하기 위해 탄생한 동기화 전용 훅입니다.
React 18의 릴리즈는 단순한 기능 추가가 아니라, 개발자들에게 위와 같은 거대한 선언을 던진 것과 다름없었습니다. 그리고 이 선언은 곧 다가올 19버전의 엄청난 API 폭격과 혁명적인 새로운 컴포넌트 패러다임에 대한 강력한 복선이었습니다.
2024 - 2026년: 동시성의 만개와 React Compiler, React 19 ~ 19.2
React 18이 동시성의 무기를 쥐여주었지만, 개발자들은 곧 클라이언트 렌더링(CSR) 환경이 가진 근본적이고 물리적인 한계를 체감하게 됩니다.
바로 첫 페이지 렌더링(Initial Load) 시점의 딜레마입니다.
사용자 인터랙션 이후의 업데이트라면 useTransition 등을 활용해 렌더링 우선순위를 뒤로 미루는(Pause) 것이 아주 훌륭하게 동작합니다. 하지만 맨 처음 빈 페이지에 접속해서 전체 화면을 그릴 때는 어떨까요?
결국 브라우저는 그 거대한 JS 번들을 다 다운로드하고, 가장 위쪽 헤더부터 가장 아래쪽 푸터까지 모든 컴포넌트의 Render Phase를 한 번씩은 다 거쳐야만 화면이 나옵니다. 첫 렌더링 시점에서는 무엇을 먼저 그리고 미루든 결국 전체 연산량은 똑같은 '제로섬(Zero-sum)' 게임인 셈입니다.
당연히 하이드레이션(Hydration) 전까지는 사용자가 버튼을 눌러도 반응조차 없는 먹통 상태(TTI 저하)가 지속됩니다. 첫 페이지 로딩 속도와 즉각적인 인터랙션을 높이는 데 있어 클라이언트 안에서의 우선순위 제어만으로는 한계에 부딪힌 것입니다.
이 물리적인 한계를 부수고, 처음부터 가장 가볍고 효율적으로 화면을 사용자에게 쏘아주기 위해 등장한 패러다임이 바로 React Server Components(RSC) 입니다.
추천 레퍼런스: Dan Abramov의 React Server Components 소개 (2020.12)
React Compiler의 등장: "Memoization의 자동화"
React 19에서 RSC와 더불어 절대 빼놓을 수 없는 거대한 패러다임 변화는 바로 **React Compiler(과거 명칭 React Forget)**의 등장입니다. 동시성 자체라기보다는 동시성 렌더링의 성능을 극대화하기 위한 가장 강력한 지원군이라고 볼 수 있습니다.
과거에는 컴포넌트 트리가 무거워지면 불필요한 렌더링을 막기 위해 개발자가 직접 useMemo, useCallback, React.memo를 수동으로 겹겹이 발라야 했습니다. 최적화를 안 하면 엔진이 비명을 지르고, 너무 많이 하면 코드의 복잡성이 폭발하는 딜레마였습니다.
React 19의 Compiler는 이 번거로운 종속성(Dependency) 추적과 캐싱을 빌드 타임(Build-time)에 자동으로 분석하고 끼워 넣어 줍니다. 개발자는 본질적인 UI 로직에만 집중하고, 엔진 뒤에서는 가장 최적화된 최소한의 렌더링(O(1)에 가까운 변경)만 일어나도록 자동화된 시대를 연 것입니다.
useAPI의 도약: 기존 훅의 규칙(최상단 호출)을 깨부순 파격적인 녀석입니다. 이제 if문이나 for문 안에서도use(Promise)형태로 값을 꺼내어 쓸 수 있으며, Pending 상태일 때 이를 자동으로 Suspense가 낚아챕니다. 동시성 렌더링 엔진과 가장 완벽하게 맞물려 돌아가는 API입니다.- Action UI State의 혁신: 19.2에 이르는 동안 폼 제출이나 낙관적 업데이트(Optimistic UI) 같은 까다로운 비동기 Action 처리를
useActionState,useFormStatus등으로 감싸, 복잡했던 로딩/에러/데이터 처리를 단 몇 줄의 동시성 안전한(Concurrency-safe) 코드로 제압했습니다.
마치며
2013년 v0.4의 이단아로 출발했던 React. 쓸모없는 찌꺼기들을 정리한 15 버전, 보이지 않는 곳에서 엔진을 완전히 갈아엎은 16의 Fiber, 그리고 클래스에서 함수로 전황을 뒤엎은 사건. 초기 Suspense로 동시성의 맛을 보여준 뒤 긴 겨울을 견딘 17 안정기. 그리고 18을 거쳐 마침내 19.2에서 동시성 렌더링과 RSC의 잠재력을 활짝 터트리기까지.
10년이 넘는 이 아름다운 타임라인은 그저 문법이 어떻게 달라졌는지를 나열한 것이 아닙니다. **"앱이 거대해지더라도, 사용자에게는 끊김 없는 마법 같은 60fps 렌더링 경험을 제공하겠다"**는 명확한 철학과 고뇌의 산물입니다.
앞으로 React 코드를 작성할 때 이 거대한 철학의 맥락을 떠올리지 못하고, 그저 무비판적으로 최신 API만 가져다 쓰는 것은 개발자로서 참 아쉬운 일일 것입니다. 코드를 그저 복사해서 붙여넣고 AI가 모든 것을 만들어주는 이른바 '대딸깍 시대' 속에서도, 이렇듯 기술의 본질적인 역사를 꿰뚫고 올바른 길을 똑바로 제시할 수 있는 주도적인 개발자가 되기를 스스로 다짐해 봅니다.
새 글 알림 받기
실무에서 바로 써먹을 수 있는 개발 팁과 경험담을 받아보세요
개인정보는 뉴스레터 발송 목적으로만 사용되며, 언제든 구독을 해지할 수 있습니다.