Thumbnail

6분

React 연대기

React 이해를 위해 React 역사 등을 정리해본 글입니다.

React의 중요한 업데이트와 변화에 대한 요약

React는 계속해서 진화하고 있으며, 이를 통해 웹 개발을 더욱 간편하고 효율적으로 만드는 데 기여하고 있습니다.

React 팀은 계속해서 새로운 기능을 개발하고, 커뮤니티의 피드백을 통해 라이브러리를 개선하고 있습니다.

아래는 React의 주요 업데이트와 변화에 대해 요약해보았습니다.

React 0.x (2013-2015)

Legend is released

이 초기 버전에서는 React의 핵심 원칙과 기능이 정립되었습니다. 가장 중요한 것은 "Virtual DOM"과 "JSX"라는 두 가지 중요한 개념의 도입이었습니다.

Virtual DOM은 웹 페이지를 더 빠르게 렌더링하기 위한 방법으로, 페이지의 상태를 메모리에 저장하고 변화가 발생하면 가상 DOM을 사용해 최소한의 변경만 실제 DOM에 반영합니다.

JSX는 JavaScript 안에서 HTML 코드를 작성할 수 있게 해주는 문법으로, React 컴포넌트를 더 직관적으로 작성할 수 있게 해줍니다.

Why Virtual DOM?

Mutation 대신 Re-Creation

React의 Virtual DOM은 여러 프론트엔드 라이브러리 및 프레임워크의 아이디어를 기반으로 만들어진 것이라고 볼 수 있습니다. 그러나 가장 큰 영향을 준 것은 프론트엔드 개발의 주요 문제점들과 웹 애플리케이션의 성능 최적화를 위한 전략들에서 왔습니다.

  1. DOM 조작의 비용: DOM의 수정은 비용이 많이 듭니다. 브라우저는 DOM의 변화를 감지하고, 레이아웃을 계산하고, 요소를 다시 그리는 등의 일련의 과정을 거칩니다. 이 과정은 복잡한 애플리케이션에서는 성능 저하를 가져올 수 있습니다.
  2. 빈번한 UI 업데이트: 웹 애플리케이션은 사용자의 인터랙션에 따라 UI를 빈번하게 업데이트해야 합니다. 이를 최적화하지 않으면 애플리케이션의 반응성이 떨어질 수 있습니다.

React는 이 두 문제를 해결하기 위해 Virtual DOM을 도입했습니다. Virtual DOM은 실제 DOM의 복사본으로, 변화를 이 복사본에 먼저 적용한 후, 실제 DOM에 필요한 변경 사항만을 최적화된 방식으로 적용하는 방법을 제공합니다. 이렇게 함으로써 불필요한 DOM 조작을 최소화하고 애플리케이션의 성능을 향상시킵니다.

이러한 아이디어는 이전에도 존재했지만, React는 이를 효과적으로 구현하여 널리 퍼지게 한 라이브러리입니다. 가상 DOM의 개념은 이후 다른 많은 프레임워크와 라이브러리에게 영향을 미쳤고 많은 곳에서 사용되고 있습니다.

JSX inspired by XHP

XHP: HTML을 PHP 안에서 사용하기 위한 문법

JSX는 JavaScript와 HTML/XML의 구문을 섞는 것을 가능하게 하는데, 이 아이디어는 일부 웹 기술에서 영감을 받았습니다. 가장 중요한 영향력은 XHP와 E4X (ECMAScript for XML)에서 왔습니다.

  • XHP: XHP는 Facebook에서 PHP를 위해 만든 HTML 컴포넌트 프레임워크입니다. 이것은 HTML 구문을 PHP 코드 내부에서 직접적으로 사용할 수 있게 만들어, UI를 더 명확하게 표현할 수 있게 도와줍니다. 이것은 JSX가 JavaScript 내에서 UI를 표현하는 방법에 대한 기본적인 아이디어를 제공했습니다.
  • E4X: E4X는 ECMAScript의 XML 확장으로, JavaScript에서 XML 구문을 직접적으로 사용할 수 있게 해줍니다. 이것은 JSX가 JavaScript와 HTML/XML 구문을 어떻게 섞을 수 있는지에 대한 아이디어를 제공했습니다.

JSX는 이러한 기술에서 영감을 받았지만, React와 함께 사용하기 위해 특별히 설계되었습니다. 이것은 React 컴포넌트의 선언적인 표현을 가능하게 하며, 컴포넌트 구조를 보다 명확하게 표현하고 가독성을 향상시킵니다. 이로 인해 개발자는 UI의 복잡성을 효과적으로 관리할 수 있도록 도와줍니다.

React 15 (2016)

React Fiber

이 업데이트에서는 React Fiber라는 새로운 코어 아키텍처가 도입되었습니다. Fiber는 기존 'Stack' 렌더러와 비교해 더 빠른 반응성을 제공하며, 특히 많은 양의 업데이트가 동시에 발생할 때 더 효율적으로 처리할 수 있게 되었습니다.

또한 이 버전에서는 Error Boundaries라는 새로운 기능이 도입되었습니다. 이 기능은 컴포넌트에서 발생하는 오류를 선언적으로 캡처하고, 이를 로깅하거나 복구 코드를 실행하는 데 사용할 수 있게 되었습니다.

React 기존 Stack Reconcilation 문제

React의 기존 stack reconciler에는 주로 두 가지 제약이 있었습니다.

  • 동기적 실행: Stack reconciler는 렌더링 작업을 한 번에 완료합니다. 이는 렌더링 중에 중단이 불가능하므로, 큰 애플리케이션에서는 UI가 블로킹되어 사용자 경험이 저하될 수 있습니다. 이는 특히 애니메이션, 레이아웃, 제스처와 같은 렌더링이 빠르게 이루어져야 하는 작업에서 문제가 되었습니다.
  • 작업 우선 순위 설정 불가: 모든 업데이트가 동일한 우선순위를 가지기 때문에, 개발자는 중요한 업데이트(예: 사용자 입력에 대한 즉각적인 응답)를 덜 중요한 업데이트(예: 백그라운드 데이터 페칭)보다 우선시하는 등의 세밀한 제어를 할 수 없었습니다.

이러한 제한 사항들은 대형 애플리케이션의 성능과 사용성에 문제를 일으킬 수 있습니다. 이 문제를 해결하기 위해 React 팀은 여러 가지 실험을 진행하였고, 이러한 실험들은 비동기 렌더링, 우선 순위 기반 스케줄링 등 향상된 기능을 가진 React Fiber라는 새로운 코어 아키텍처로 이어졌습니다.

왜 React 1.0이 아닌 React 15.0 부터 인가요?

React가 0.14 버전에서 15.0으로 점프한 이유는 여러 가지 있습니다. 그 중 하나는 이전 버전에서 0.x.x 형식의 버전 체계를 사용하면서 생긴 혼란을 해소하려는 의도가 있었습니다. '0.'으로 시작하는 버전 번호는 일반적으로 "안정적이지 않은" 또는 "아직 완성되지 않은" 소프트웨어를 나타내는 데 사용되는데, React는 그 시점에 이미 널리 사용되고 있었고 많은 기능이 안정적이었습니다.

또한, React는 그 때부터 SemVer (Semantic Versioning)를 따르기 시작했습니다. React는 0.14 버전에서 15.0으로 버전 번호를 바꾸면서부터 SemVer 체계를 따르기 시작했습니다.

이러한 변경은 React의 성장과 안정성, 그리고 프로젝트의 의사소통 및 관리를 향상시키기 위한 것이었습니다.

React Native가 2015년에 공개된 것을 알고 계신가요? 더욱 놀라운 것은 현재 2023년, React Native는 아직도 0.x 버전을 사용하고 있습니다.

React 16.3 (2018)

이 업데이트에서는 새로운 생명주기 메소드가 도입되었습니다. getDerivedStateFromPropsgetSnapshotBeforeUpdate는 오래된 메소드인 componentWillMount, componentWillReceiveProps, componentWillUpdate를 대체하였습니다.

이러한 변경은 비동기 렌더링을 더 잘 지원하기 위한 것이었습니다. 또한, Context API가 리디자인되어 사용하기 더욱 간편해졌습니다.

Context API 리디자인

React 16.3에서 Context API는 매우 큰 변화를 겪었습니다. 이전의 Context API는 실험적인 기능으로, 비록 강력하지만 사용이 권장되지 않았습니다. 그 이유는 React의 라이프사이클과 잘 통합되지 않아 예기치 못한 문제를 일으키거나, 애플리케이션의 리렌더링 효율을 저하시킬 수 있기 때문입니다.

Screenshot of React 16.3 life cycle

그러나 16.3버전에서는 이 Context API가 새롭게 디자인되어 더 안정적이고 예측 가능한 방식으로 사용할 수 있게 되었습니다.

Context API는 React 컴포넌트 트리 전체에 걸쳐 데이터를 공유할 수 있게 해주는 기능입니다. Redux와 같은 상태 관리 라이브러리 없이도 상위 컴포넌트에서 하위 컴포넌트로 prop를 "Pass"시키는 것 없이 데이터를 공유할 수 있게 되었습니다.

React 16.8 (2019)

Hooks

이 버전에서는 Hooks라는 새로운 개념이 도입되었습니다. Hooks는 함수형 컴포넌트에서도 state와 생명주기 메소드를 사용할 수 있게 하여, 클래스 컴포넌트의 필요성을 크게 줄여주었습니다. Hooks의 도입으로 인해 React 코드는 더 간결하고 가독성이 높아졌으며, 코드의 재사용성도 향상되었습니다.

Hooks가 도입된 이유 == 클래스 컴포넌트의 한계점과 복잡성

React 클래스 컴포넌트 사용에 몇 가지 불편함을 가지고 있었습니다.

  1. 복잡한 컴포넌트를 이해하기 어렵다: 클래스 컴포넌트에서는 관련 없는 로직이 종종 함께 묶이곤 했고, 생명주기 메소드를 기반으로 분리된 상태 관련 로직을 다시 결합해야 했습니다. 또한 클래스들이 자바스크립트 코드(예: this, bind 등) 내에서 많은 혼란을 초래했고 코드 재사용과 테스트를 어렵게 만들었습니다.
  2. 컴포넌트 간 상태 로직 재사용이 어렵다: 이전에는 이 문제를 해결하기 위해 render props와 고차 컴포넌트(HOC) 같은 패턴을 사용했지만, 이 패턴들은 코드를 추적하기 어렵게 만들고, 컴포넌트 재구성을 필요로 했습니다.
  3. 코드 양이 많아진다: 클래스 컴포넌트는 render 메소드, constructor 메소드, componentDidMount 메소드 등 여러 메소드를 작성해야 하기 때문에, 함수형 컴포넌트에 비해 코드의 양이 많아집니다.

이런 이유들로 인해 React 팀은 함수형 컴포넌트에서도 상태 관리와 생명주기 메소드를 사용할 수 있게 하는 Hooks를 도입하게 되었습니다. Hooks를 통해 로직에 기반을 둔 작은 단위로 컴포넌트를 나눌 수 있게 되어, 재사용성이 높아지고 코드의 가독성이 향상되었습니다.

React 17 (2020)

이 업데이트에서는 주요한 새로운 기능보다는 React의 설치 및 업그레이드 프로세스를 개선하는 데 초점이 맞추어졌습니다.

이 업데이트에서는 React의 이벤트 위임 방식을 변경하였습니다. React 17에서는 이벤트 핸들러가 더 이상 document 객체에 연결되지 않고, 대신 React 루트(DOM 노드)에 직접 연결됩니다. 이는 React 코드와 non-React 코드가 같은 페이지에서 동시에 작동하는 경우에 문제를 일으키지 않게 합니다. React의 이벤트 시스템을 웹 표준에 더 가깝게 만들며, 여러 버전의 React를 한 페이지에 함께 사용할 때 생기는 문제를 줄여주었습니다.

// React 17
const rootNode = document.getElementById("root")
ReactDOM.render(<App />, rootNode)

An image for React delegation between React 16 and React 17

또한 이전의 JSX Transform은 "React"를 자동으로 import했지만, React 17의 새로운 JSX Transform은 이를 수행하지 않습니다. 이로 인해 번들 사이즈가 줄어들고, 개발자가 실제로 사용하는 것들만 import하게 되었습니다.

componentWillUpdate, 그리고 componentWillReceiveProps와 같은 레거시 생명주기 메소드가 React 17에서는 deprecated되었습니다.

React의 DOM 이벤트 위임

React는 일반적으로 실제 DOM 이벤트를 직접적으로 관리하지 않고, 'Virtual DOM'에서 발생한 이벤트를 조정하거나 위임하는 방식을 사용합니다. 이 방식은 코드의 가독성을 향상시키고, React 컴포넌트의 로직을 더욱 단순화하는 데 도움이 됩니다.

이벤트 위임은 이벤트를 적절한 컴포넌트에 전달하는 과정으로, 모든 이벤트 핸들러가 실제 DOM 노드에 연결되는 대신 최상위 레벨에서 모든 이벤트를 수신하고 처리합니다. 이 방식은 이벤트 핸들러가 필요한 수만큼의 이벤트 리스너를 추가하지 않고도 많은 이벤트를 처리할 수 있도록 해주어, 메모리 사용량을 줄이고 성능을 향상시킵니다.

기존의 React (React 17 이전)에서는 문서의 최상위 수준(root)에서 모든 이벤트를 수신하고 처리하도록 이벤트 위임을 구현했습니다. 즉, 실제로는 모든 이벤트 핸들러가 document 객체에 연결되었습니다.

React 17에서는 이벤트 핸들러가 더 이상 document 객체에 연결되지 않고, 대신 React 루트(DOM 노드)에 직접 연결됩니다

React의 DOM 이벤트 처리 방식

React는 브라우저의 DOM 이벤트 처리를 추상화하고 독특한 방식으로 처리합니다. React의 이벤트 처리 시스템은 다음의 주요 부분으로 구성되어 있습니다

  • 합성 이벤트 (Synthetic Event): React는 브라우저 네이티브 이벤트를 직접 다루지 않고, 대신에 Synthetic Event를 사용합니다. Synthetic Event는 브라우저의 네이티브 이벤트와 인터페이스가 유사하지만, 브라우저 간 일관성을 제공하고, 브라우저의 이벤트 시스템의 성능을 개선하는 등의 추가적인 이점을 제공합니다.
  • 이벤트 위임 (Event Delegation): React는 모든 이벤트 핸들러를 실제 DOM 노드에 연결하지 않습니다. 대신에, 모든 이벤트는 React에서 관리하는 최상위 노드(React 17부터는 React 루트(<div id="root"></div>))에서 핸들링됩니다. 이는 "이벤트 위임" 방식으로, 이벤트 버블링을 활용하여 효율적으로 이벤트를 관리합니다. 이 방식은 이벤트 핸들러가 필요한 수만큼의 이벤트 리스너를 추가하지 않고도 많은 이벤트를 처리할 수 있도록 해주어, 메모리 사용량을 줄이고 성능을 향상시킵니다.
  • 이벤트 핸들러: React에서 이벤트를 처리하려면, 해당 컴포넌트의 이벤트 핸들러에 함수를 할당합니다. 이 함수는 주어진 이벤트가 발생할 때 호출됩니다. 그리고 이벤트 핸들러는 React 이벤트 객체를 인자로 받습니다.

React 18 (2022)

Concurrent!, Concurrent!, Concurrent!

React 18 변화를 한 단어로 정리하자면 Concurrency 입니다. 대부분의 새로운 기능은 React에서 동시성을 제공하기 위한 기능입니다.

이 버전에서는 새로운 기능인 'Concurrent Mode'와 'React Server Components'가 도입되었습니다.

Concurrent Mode는 앱의 응답성을 높이기 위해 여러 우선순위의 업데이트를 동시에 처리할 수 있게 합니다.

React Server Components는 서버에서 컴포넌트를 렌더링하고 결과를 클라이언트로 전송하여 성능을 향상시키는 새로운 기능입니다.

Concurrent Mode: 동시성 모드

기존 브라우저에서 React를 사용할 때, React는 브라우저의 메인 스레드에서 렌더링을 수행합니다. 이는 브라우저가 React의 렌더링 작업을 수행하는 동안 다른 작업을 수행할 수 없다는 것을 의미합니다. 이는 특정 Task가 스레드를 blocking하는 동안 즉각적인 렌더링 반응을 얻지 못하여 앱이 느려지는 것처럼 느낄 수 있습니다.

기존 React(Stack Reconciler 시절)는 컴포넌트를 렌더링하면서 중간에 다른 작업(예: 사용자 이벤트 처리)을 수행할 수 없었습니다. 이를 위해 React Fiber를 도입했습니다. React Fiber를 통해 React는 렌더링 작업을 여러 작은 작업으로 분할하여 브라우저가 렌더링 작업을 수행하는 동안 다른 작업을 수행할 수 있는 능력을 얻었습니다. 이는 사용자 이벤트 처리 등의 우선 순위가 높은 작업을 먼저 수행하게 하는 등, 일부 동시성과 관련된 이점을 제공했습니다.

그렇다면 React Fiber를 도입한 시점부터는 동시성 모드를 사용할 수 있지 않았나요?

React Fiber를 도입한 React 16에서도 여전히 몇 가지 동시성 문제를 완전히 해결하기 어려웠습니다. 예를 들어, 여러 상태 업데이트가 동시에 발생할 때 일관성 있는 사용자 인터페이스를 유지하는 것이 어렵다는 문제가 있었습니다.

그런데 위에서 "여러 상태 업데이트가 동시에 발생할 때 일관성 있는 사용자 인터페이스를 유지하는 것이 어렵다는 문제가 있었습니다" 라는 뜻은 어떤 의미일까요?
React 애플리케이션에서는 여러 상태 업데이트가 동시에 발생하는 경우, 이러한 업데이트들은 각각 별개의 렌더링 사이클을 생성합니다. 이 경우, 사용자 인터페이스가 일시적으로 일관성 없는 상태를 보여줄 가능성이 있습니다.

이런 문제의 예로, 아래의 상황을 생각해 볼 수 있습니다:

  1. 사용자가 데이터를 불러오는 버튼을 클릭합니다. 이를 위해 상태 업데이트가 발생하여 "로딩" 스피너가 표시됩니다.
  2. 거의 동시에, 다른 부분의 코드에서 렌더링에 영향을 주는 상태 업데이트가 발생합니다. 예를 들어, 다른 데이터의 업데이트나, 사용자 인터페이스의 일부분을 업데이트하는 것 등입니다.
  3. 두 번째 상태 업데이트로 인해, React가 렌더링을 실행하고, 변경된 부분만을 업데이트합니다. 그러나 이 때, 첫 번째 상태 업데이트(데이터 로딩)가 완전히 반영되지 않았다면, 사용자는 "로딩" 스피너가 없는 화면을 보게 될 수 있습니다. 이는 일관성 없는 상태를 사용자에게 보여주는 결과를 초래합니다.

이러한 문제들은 기존의 렌더링 방식과 연관된 것이며, 이를 해결하기 위해서는 새로운 동시성 모드를 도입해야 했습니다.

즉, Fiber는 동시성을 가능하게 하는 기초적인 기술이었으며, Concurrent Mode는 이러한 기술을 바탕으로 구체적인 동시성 문제를 해결하는 데 사용되는 고수준의 기능입니다. 이 두 기술이 결합되어 React는 효과적인 동시성 관리를 제공할 수 있게 되었습니다.

Concurrent Mode는 여러 상태 업데이트를 동시에 처리할 수 있도록 하면서도, 각 상태 업데이트를 아토믹하게 적용할 수 있게 합니다. 즉, 한 상태 업데이트가 완전히 적용될 때까지, 다른 상태 업데이트로 인한 렌더링은 차단됩니다. React 18에 도입된 Automatic batching과 같은 기능을 통해 여러 상태 업데이트가 동시에 발생하는 경우에도 사용자 인터페이스의 일관성을 유지할 수 있게 되었습니다.

또한 useTransition, useDeferredValue와 같은 훅을 통해, 개발자는 사용자가 더 나은 사용자 경험을 얻을 수 있도록 상태 업데이트를 조정할 수 있게 되었습니다.

React Server Component

React Server Component

RSC란 무엇인가요?

마무리하며

React의 주요 업데이트와 변화에 대해 요약해보았습니다. 정리를 하다 보니 React가 벌써 세상에 발표된지 10년이 넘었다는 사실이 놀라웠습니다.

버전이 올라간다는 것은, 특히 메이저 버전이 올라간다는 것은 무언가 큰 변화가 있었다는 것을 의미합니다. React의 메이저 버전이 올라갈 때마다 React의 생태계는 큰 변화를 겪었습니다. 이러한 변화들은 React를 사용하는 개발자들에게 큰 영향을 미쳤고, React의 생태계를 더욱 발전시켰습니다.

React는 계속해서 발전하고 있으며, 이를 통해 웹 개발을 더욱 간편하고 효율적으로 만드는 데 기여하고 있습니다. 앞으로 언제까지 React가 지금처럼 사랑받을지는 모르겠지만, 당분간은 기대가 됩니다. React가 얼만큼 발전될지, 그리고 어떤 변화를 가져올지 기대가 됩니다.

다음은 React와 관련된 주요 발표에 대해서 정리해보고자 합니다.

reference

마지막 업데이트

7/20/2023


Avatar

JHSeo

배우는 것을 좋아하고 관심이 많은 웹 엔지니어 입니다. 느리더라도 꾸준하게 성장하려고 노력하는 개발자입니다.