Thinking in React

hyunjine·2022년 9월 28일
134
post-thumbnail

안녕하세요 이현진입니다.

Thinking in React(React로 생각하기)라는 주제로 발표를 진행하겠습니다. React로 생각한다는 표현이 조금 추상적일 수 있는데, 이 발표에는 React를 다룰 때 갖고 있어야하는 생각들을 담았습니다.

먼저 DOM에 대한 이야기부터 시작해보겠습니다. DOMDocument Object Model의 약자로, 브라우저가 HTML을 파싱하여 객체 형태로 만든 것을 말합니다.

위와 같은 HTML구조를 갖는 웹사이트가 있다고 가정해보겠습니다.
HTML은 문자열입니다.문자열은 다루기 어렵습니다.(파싱, 합치기등의 작업)

브라우저는 이 다루기 어려운 문자열을 훨씬 다루기 쉬운 객체 형태로 바꿔주고, 이 객체를 DOM이라 합니다. 개발자는 JavaScript를 이용해 DOM을 조작하여 웹 애플리케이션을 개발합니다.

하지만 JavaScript로 직접 DOM을 조작하는 것은 여러가지 단점이 따라옵니다.

  1. JavaScript로 DOM을 조작할 때 HTML의 구조를 파악하기 어렵습니다.(DOM을 직접 생성, 수정 삭제할 때 구조를 파악하기 어렵습니다.)
  2. 표준을 따르는 브라우저도 많지만, 그 안에서도 다른 동작을 가지고 있을 수 있습니다.
  3. Live Collection과 Static Collection

Live CollectionDOM API가 반환한 값이 Live한 상태를 의미합니다. 즉, DOM의 실시간 변경사항에 따라 언제든 값이 바뀔 수 있습니다. 반면에 Static CollectionDOM의 실시간 변경사항에 따라 값이 바뀌지 않습니다.

DOM API는 반환하는 값이 Live 할 수도 있고, Static 할 수도 있기 때문에 "일관성이 없다"라고 표현합니다.

DOM API가 일관성이 없고 사용하기 불편하다면 어떻게 해야할까요?

가장 간단한 답으로는 DOM API을 사용하지 않는 방법이 있습니다. DOM API를 직접 사용하지 않고 중간에 매개체를 두어서 DOM을 조작할 수 있습니다.

이 매개체가 React입니다.

React는 DOM조작과 같이 어려운 일은 자신이 하고, 개발자에게는 훨씬 편리한 API를 제공해줍니다.

마치 HTML이라는 문자열을 직접 다루기 어렵기 때문에 DOM이라는 객체를 만든 것 처럼, DOM을 직접 다루기 어렵기 때문에 React를 만든 것이라고 할 수 있습니다.

React는 웹 애플리케이션의 UI를 재사용 가능한 컴포넌트들을 모아서 구성합니다. 각 컴포넌트에는 데이터 모델이 존재합니다. 애플리케이션의 UI와 상호작용하려면 UI에 내재하는 데이터 모델을 바꿈으로써 상호작용할 수 있습니다.

데이터 모델을 React에서는 State(상태)라고 합니다. 상태란 주어진 시간에 대해 시스템을 나타내는 것으로 언제든지 변경될 수 있고 상태가 업데이트되면 React 컴포넌트는 렌더링됩니다.

렌더링이란 React가 컴포넌트에게 현재 PropsState에 기반하여 UI에 어떻게 보여지고 싶은지 알려달라고 요청하는 과정입니다. 렌더링은 간단히 말해서 함수 컴포넌트를 호출하는 것이라고 할 수 있으며, 함수에서 반환하는 JSX는 시간에 따른 UI의 스냅샷과 같습니다.

컴포넌트가 위와 같은 트리 구조를 갖고 있다고 해보겠습니다. 빨간색 컴포넌트는 상태가 업데이트된 컴포넌트입니다. 상태가 업데이트되면 컴포넌트는 업데이트가 필요하다는 표시를 합니다.(빨간색 React로고)

React는 상태 업데이트를 감지하면 렌더링을 큐(queue)에 넣습니다.

React는 트리의 최상단(A)부터 렌더 패스(Render Pass)를 시작합니다. A에는 업데이트가 필요하다는 마크가 없는 것을 보고 지나칩니다.

다음은 B를 방문합니다. React는 B에 업데이트가 필요하다는 마크가 있는 것을 보고 렌더링합니다. 여기서 중요한 점은 React는 기본적으로 부모 컴포넌트가 렌더링되면, 모든 자식 컴포넌트를 재귀적으로 렌더링한다는 점입니다.

이에 따라 CD는 업데이트가 필요하다는 마크가 없지만 부모 컴포넌트인 B가 렌더링되었기 때문에 CD를 렌더링합니다.

다음으로 남은 E를 체크하고 업데이트가 필요하다는 마크가 없으므로 아래로 내려가서 업데이트가 필요하다는 표시가 있는 F를 발견하고 F를 렌더링합니다.

여기서 컴포넌트 트리 안에 있는 컴포넌트들 중에서는 C, D와 같이 직전과 똑같은 렌더링 결과물을 반환하는 컴포넌트가 존재합니다. 따라서 같은 결과물을 반환하는 컴포넌트는 DOM에 반영할 필요가 없습니다. 하지만 렌더링의 결과물이 같다는 것을 어떻게 알 수 있을까요?

React는 VirtualDOM을 활용합니다. 기존 VirtualDOM과 상태 업데이트 후의 VirtualDOM에서 바뀐 부분만을 계산(diffing)하여 실제 바뀐 부분만 DOM에 적용합니다.

이를 React에서 Reconciliation(재조정)이라고 합니다.

이러한 사실을 바탕으로 렌더링을 두 단계로 쪼갤 수 있습니다.

  • Render phase(렌더 단계): 컴포넌트를 렌더링하고 변경 사항을 계산하는 모든 과정이 이루어지는 단계(VirtualDOM 조작 단계)
  • Commit phase(커밋 단계): 변경 사항을 실제 DOM에 적용하는 단계

렌더링과 DOM을 업데이트하는 것은 같은 것이 아니며 컴포넌트는 가시적인 변화가 없어도 렌더링될 수 있습니다.

렌더링은 기본적으로 상태 업데이트에 의해 발생됩니다. 따라서 React 애플리케이션은 상태 관리를 어떻게 하느냐에 따라 애플리케이션의 미래가 결정됩니다. 불필요하거나 중복된 상태는 버그의 일반적인 원인이 될 수 있습니다.

즉, 적절한 상태를 적절한 컴포넌트에 배치시켜야합니다.

Props(properties)는 컴포넌트간에 값을 전달할 때 사용합니다.(데이터 전달)

예를 들어 하위 컴포넌트 두개가 같은 상태(현진)를 갖는데 두 상태가 항상 함께 변경되기를 원할 수 있습니다.

같이 변경되어야하는 두 상태는 중복 상태이므로 둘 다에서 상태를 제거하고 가장 가까운 공통 부모로 상태를 이동시킨후에 props를 통해 전달합니다. 이를 상태 끌어올리기(Lifting State Up)이라고 합니다.

상태를 끌어올린 후에 하위 컴포넌트에게 Props로 전달합니다.

React에서는 데이터의 흐름이 상위 컴포넌트에서 하위 컴포넌트로 한 방향으로만 흐릅니다.(단방향 데이터 흐름, Unidirectional Data Flow)

현진이라는 상태를 이현진으로 바꾸고 싶다고 해봅시다.
하위 컴포넌트에서 상위 컴포넌트의 상태를 변경하고 싶다면 어떻게 해야할까요?

Props로 상태를 업데이트하는 함수를 전달하여 하위 컴포넌트에서 상태를 업데이트하는 함수를 호출하면됩니다.

이렇게 상태를 업데이트하면 하위 컴포넌트에서 상위 컴포넌트의 상태를 업데이트할 수 있습니다. 이렇게 역방향 데이터 흐름(Inverse Data Flow)을 추가할 수 있습니다.

지금까지 상태를 애플리케이션에 분배하고 다뤄봤습니다. 브라우저내에서 모든걸 처리할 수 있다면 클라이언트 상태로도 충분하지만 대다수의 애플리케이션은 서버 상태가 존재합니다.

서버상태는 다음과 같은 특성을 지닙니다.

  • 서버 상태는 사용자의 제어를 벗어난 위치에서 원격으로 유지된다.
  • 비동기 요청을 통해 fetching또는 updating이 가능하다.
  • 소유권을 공유한다. 즉 사용자 모르게 다른 사용자가 변경할 수 있다.
  • 시간이 지남에 따라 stale또는 outdated된다.

React는 UI 라이브러리이기 때문에 데이터를 fetching하는것에는 관심이 없습니다. 단지 fetching한 데이터를 UI에 반영시키는 것에만 관심이 많습니다.

React는 상태에 따라 UI를 어떻게 렌더링할지에 관심이 있기 때문에 서버 상태를 다루려면 여러가지 상태를 정의해야합니다. Loading, Error, Success 상태를 정의하여 각각의 상태별로 매 렌더링마다 UI의 스냅샷을 찍어서 보여줍니다.

React에서는 상태를 업데이트하는 로직이 복잡해지면 reducer를 사용하듯이, 컴포넌트 내부에 Loading, Error, Success와 같은 상태를 두지 않고 전역 상태 관리자인 Redux를 사용하여 상태를 업데이트하는 로직을 컴포넌트 외부로 빼내서 비동기 요청에 대한 렌더링 로직을 작성했습니다.

여기서 우리는 의문을 제기해야할 필요가 있습니다. 전역 상태관리 라이브러리인 Redux의 역할이 과연 API 요청에 대한 각각의 상태를 정의해 렌더링 로직을 작성하는 것인가?

답은 아니라고 생각합니다. 전역 상태관리자의 역할은 전체 애플리케이션에서 정말 전역적으로 관리해야하는 상태(theme, 사이드바 상태등)를 가지고 있어야합니다.

기존에는 API 요청의 상태(Loading, Error, Success)에 따라 적합한 UI를 보여주기 위해 컴포넌트 외부에 수많은 보일러 플레이트 코드를 작성해야했습니다.

React Query는 이를 해결합니다. React Query의 역할은 명확합니다.

서버 상태를 관리하기위해 필요했던 보일러플레이트 코드를 제거한다. 그리고 단 몇줄의 코드로 대체한다.

React Query를 사용하면 여러 상태를 정의해야하는 문제는 해결됩니다. 하지만 컴포넌트가 isLoading과 같은 상태일 때 반환할 UI를 정의해줘야합니다.

이는 UI의 일관성을 해칩니다.

Suspense는 이를 해결합니다. Suspense의 목표는 서버상태를 읽어오는 것을 React의 props와 state처럼 쉽게 다루는 것입니다.

이렇게 비동기적으로 데이터를 불러오는 컴포넌트를 Suspense감싸고 fallback으로 보여줄 컴포넌트를 전달합니다. 이렇게하면 기존 UI의 로딩 상태를 명령형(imperative) 방식으로 정의해야했던 것을 React의 패러다임에 맞게 선언적(declarative)인 방식으로 바꿀 수 있습니다.

Suspense는 단순히 로딩 스피너가 아닙니다. React 18에서는 Suspense를 이용한 두개의 SSR(Server Side Rendering)기능이 추가됩니다. HTML Streaming과 Selective Hydration

지금까지 React에 관한 다양한 내용들을 다루었는데 마지막으로 React v18에 대해서 이야기해 보려고 합니다.

2161일. React팀이 React v18.0.0을 릴리즈하는데 걸린 시간입니다.(React팀이 Async rendering이라는 개념을 소개한 이후 2161일 걸림) 왜 이렇게 오래걸렸을까요?

React팀의 목표는 하나였습니다. 성능이 좋은 React를 만들어서 수백만개의 React로 만들어진 웹사이트 성능을 높인다.

가장 큰 문제는 React가 아닌 React를 만든 언어에 있었습니다.

React는 JavaScript 위에서 만들어졌기 때문에 JavaScript의 제약을 따를 수 밖에 없습니다. 특히 JavaScript가 브라우저 위에서 동작하는 방식을 따릅니다.

브라우저의 메인 스레드는 싱글 스레드로 한번에 하나의 작업만 처리할 수 있습니다. HTML을 파싱하거나 JavaScript를 실행하거나 화면에 보이는 내용을 렌더링하는데 사용됩니다.

React를 비롯한 대다수의 UI 라이브러리 작동 방식도 이 한계(브라우저의 메인 스레드)에 종속될 수 밖에 없습니다. React도 화면에 그리기 위한 내부 연산, 즉 렌더링을 시작해서 화면을 완성할 때까지 실행을 멈출수 없습니다. 이를 React 팀에서는 블로킹 렌더링이라고 부릅니다.

정확히는 React 18 이전까지는 그랬습니다.
React 18에서는 동시성 기능이 추가되었습니다.(Concurrent features)

동시성이란 두개 이상의 독립적인 작업을 잘게 나누어 Context Switching을 하며 동시에 실행되는 것처럼 보이도록 프로그램을 구조화하는 방법입니다.

동시성 기능을 활용하면 렌더링을 잘게 쪼개어 상태 업데이트에 우선순위를 두어 좀더 긴급한 상태 업데이트를 먼저 수행할 수 있습니다.

동시성 기능은 마치 고속차선과 일반차선을 두는 것과 같습니다. 고속차선으로는 좀더 긴급한 상태업데이트가 지나갈 수 있도록 하고, 일반차선으로는 좀 덜 긴급한 상태업데이트가 지나갈 수 있도록 개발자가 조절할 수 있습니다.

이제 마지막으로 발표내용을 정리해보겠습니다.

  • React는 DOM 조작의 문제점을 해결하기위해 만들어졌다.
  • React는 Reconciliation(재조정) 과정을 통해 DOM을 업데이트한다.
  • React의 핵심은 상태 관리이다.
  • React에서 Concurrent Feature를 활용해 렌더링 우선순위를 정할 수 있다.

React는 UI를 변수에 저장할 수 있으며 값으로 전달할 수 있습니다. 즉 React는 value UI입니다.

React의 핵심 원칙은 UI는 값이라는 것입니다.

감사합니다.

발표자료

Thinking in React.pdf
Thinking in React.key

16개의 댓글

comment-user-thumbnail
2022년 9월 29일

좋은 글 감사합니다

1개의 답글
comment-user-thumbnail
2022년 9월 30일

와 정말 멋진 글이에요 ! 첨부화면이 예쁘고 정리도 깔끔하네요 =D

1개의 답글
comment-user-thumbnail
2022년 10월 1일

리액트 뉴비인데
정리가 너무 잘 된 글인 것 같아요 감사합니다!

1개의 답글
comment-user-thumbnail
2022년 10월 1일

그동안 정리했던 dom의 문제점과 react의 렌더링, state와 props, client state - server state, suspense들의 개념들을 한번에 짚을 수 있어서 좋았습니다 :) context switching이 os에서만 쓰이는 개념이라고 생각했는데 리액트 18에서 이렇게 생각해볼 수 있다는 점이 너무 신기했습니다. 너무 잘 읽었어요!!

1개의 답글
comment-user-thumbnail
2022년 10월 4일

18버전 suspense는 명확해서 좋네요

1개의 답글
comment-user-thumbnail
2022년 10월 4일

글 잘 읽었습니다 감사합니다 :)

1개의 답글
comment-user-thumbnail
2022년 10월 5일

정리가 깔끔해요~

1개의 답글
comment-user-thumbnail
2022년 10월 5일

정리가 엄청 깔끔하네요. 좋은 글 감사합니다.

1개의 답글