사전 React
구현체

React

gabury1

React 는 화면을 만드는 JavaScript 라이브러리입니다. 컴포넌트라는 조각을 만들어 서로 끼워 맞추는 방식으로 화면을 짭니다. 상태를 바꾸면 React 가 화면을 거기에 맞춰 다시 그립니다. 웹과 네이티브 앱에 같은 방식을 씁니다.

상세

공식 사이트가 컴포넌트의 예로 드는 이름은 Thumbnail, LikeButton, Video 입니다. 컴포넌트 하나는 자기 상태를 스스로 관리하는 캡슐이라고 공식 저장소 README 가 적습니다. 컴포넌트 로직은 템플릿이 아니라 JavaScript 로 씁니다.

그래서 상태를 DOM(Document Object Model, 문서 객체 모델) 바깥에 둘 수 있다고 같은 문서가 적습니다. 상태가 화면 쪽이 아니라 JavaScript 쪽에 남는다는 뜻입니다.

대상은 웹 화면만이 아닙니다. 서버 렌더에는 Node 를, 모바일 앱에는 React Native 를 쓴다고 README 가 적습니다.

컴포넌트가 반환하는 마크업 문법은 JSX(JavaScript 문법 확장)라고 부릅니다. React 가 퍼뜨린 문법이라고 공식 문서가 적습니다. JSX 마크업을 관련 렌더링 로직 가까이에 두면 React 컴포넌트를 만들고 유지하고 지우기 쉬워진다는 것이 그 자리에 적힌 이유입니다.

포기한 것

React 의 정체는 안 하기로 한 것에서 드러납니다. 공식 문서가 그 결정들을 스스로 적어 뒀습니다.

라우팅과 데이터 페칭

공식 사이트는 React 를 라이브러리라고 못 박습니다. 컴포넌트를 조립하게 해주지만 라우팅과 데이터 페칭을 어떻게 할지는 규정하지 않는다고 적습니다. 그래서 React 로 앱 전체를 만들려면 Next.js 나 React Router 같은 풀스택 React 프레임워크로 시작하기를 권합니다.

빠져나갈 자리도 적혀 있습니다. 기존 프레임워크가 앱의 제약을 잘 받쳐주지 못하거나, 직접 프레임워크를 만들고 싶거나, React 앱의 기본만 배우고 싶은 경우입니다. 그러면 맨바닥에서 React 앱을 만들 수 있다고 적습니다.

대신 얻은 것이 README 의 "Learn Once, Write Anywhere" 입니다. 나머지 기술 스택에 가정을 두지 않으므로 기존 코드를 다시 쓰지 않고 새 기능만 React 로 만들 수 있습니다.

상태를 제자리에서 고치는 일

상태에는 객체를 포함한 어떤 JavaScript 값이든 담을 수 있습니다. 그러나 React 상태에 든 객체를 직접 바꾸면 안 된다고 공식 문서가 적습니다. 갱신하려면 새 객체를 만들거나 기존 객체를 복사해야 합니다. 그런 다음 그 복사본을 상태로 설정합니다.

문서는 기술적으로는 객체 내용을 바꿀 수 있다고 인정합니다. 그것을 뮤테이션이라고 부릅니다. 그러면서도 React 상태의 객체를 숫자·불리언·문자열처럼 불변인 것으로 취급하라고 적습니다. 바꾸지 말고 언제나 교체하라는 것입니다. 상태는 읽기 전용으로 다룹니다.

이 금지의 근거는 순수성 요구입니다. React 는 여러분이 쓰는 모든 컴포넌트가 순수 함수라고 가정한다고 적습니다. 같은 입력이면 언제나 같은 JSX 를 반환해야 한다는 뜻입니다.

세밀한 반응성

값 하나가 바뀔 때 그 값을 쓰는 자리만 골라 갱신하는 길은 택하지 않았습니다. memo 문서는 부모가 다시 렌더되면 React 가 보통 그 컴포넌트도 다시 렌더한다고 적습니다. 컴포넌트 함수를 통째로 다시 호출하는 쪽입니다.

그래서 건너뛰기를 손으로 지정하는 API(Application Programming Interface, 응용 프로그램 인터페이스)가 따로 있습니다. memo 로 감싼 컴포넌트는 새 props 가 이전 props 와 같은 한 부모가 다시 렌더돼도 다시 렌더되지 않습니다. 이런 컴포넌트를 메모이즈됐다고 부릅니다. 문서는 memo 를 쓰는 일을 이 컴포넌트가 순수 렌더링 요구를 지킨다고 React 에 알리는 것으로 설명합니다.

같은 문서가 각주를 하나 답니다. React 컴파일러가 모든 컴포넌트에 memo 에 해당하는 것을 자동으로 적용해 손으로 하는 메모이제이션의 필요를 줄인다고 적습니다.

사용자 영역의 기능

Design Principles 문서는 사용자 영역에서 구현할 수 있는 기능을 넣는 것을 대체로 꺼린다고 적습니다. 쓰이지 않는 라이브러리 코드로 앱을 부풀리고 싶지 않다는 이유입니다. 예외도 적어 뒀습니다. 많은 컴포넌트가 어떤 기능을 서로 호환되지 않거나 비효율적인 방식으로 구현하는 것이 보이면, 그 기능을 React 자체에 넣는 쪽을 택하기도 합니다.

비상구도 같은 문서에 있습니다. 앱을 만드는 데 쓸모 있는 패턴을 선언형으로 표현하기 어려우면 명령형 API 를 제공합니다. 여러 앱에서 필요하다고 확인된 것에 제대로 된 API 를 못 찾으면, 나중에 없앨 수 있는 한 수준에 못 미치더라도 동작하는 임시 API 를 내놓습니다.

바꾸지 않기로 한 것도 있습니다. 같은 문서가 API 안정성을 중시한다고 적습니다. Facebook 에서 React 를 쓰는 컴포넌트가 5만 개를 넘습니다. 그래서 공개 API 나 동작을 바꾸는 것을 대체로 꺼린다고 적습니다.

예시

상태 하나를 가진 컴포넌트

jsx
import { useState } from 'react';

function SearchableVideoList({ videos }) {
  const [searchText, setSearchText] = useState('');
  const foundVideos = filterVideos(videos, searchText);
  return (
    <>
      <SearchInput value={searchText} onChange={newText => setSearchText(newText)} />
      <VideoList videos={foundVideos} emptyHeading={`No matches for "${searchText}"`} />
    </>
  );
}

useState 는 상태 변수를 컴포넌트에 더해주는 React 훅입니다. 시그니처는 const [state, setState] = useState(initialState) 입니다. 컴포넌트 최상단에서 부릅니다. 반환된 배열의 첫 칸이 현재 값, 둘째 칸이 설정 함수입니다. 위 코드에서 searchText 가 값이고 setSearchText 가 설정 함수입니다.

React 컴포넌트 이름은 언제나 대문자로 시작해야 한다고 문서가 적습니다. HTML(HyperText Markup Language, 하이퍼텍스트 마크업 언어) 태그는 소문자여야 합니다. SearchInput 과 VideoList 가 대문자로 시작하는 이유입니다.

화면에 붙이는 진입점

jsx
import { createRoot } from 'react-dom/client';

const domNode = document.getElementById('root');
const root = createRoot(domNode);
root.render(<App />);

createRoot 는 브라우저 DOM 노드 안에 React 컴포넌트를 표시할 루트를 만듭니다. 시그니처는 const root = createRoot(domNode, options?) 입니다. 루트를 만들면 React 가 그 안의 DOM 관리를 넘겨받습니다. 루트를 만든 뒤 root.render 를 불러야 그 안에 컴포넌트가 표시됩니다.

채택한 곳

Design Principles 문서가 이름을 댑니다. Facebook 에서 React 를 쓰는 컴포넌트가 5만 개를 넘습니다. Twitter 와 Airbnb 를 포함한 다른 회사들도 React 를 많이 쓴다고 적습니다. 왜 React 를 골랐는지는 그 문서가 적지 않습니다.

동작

화면을 요청하고 내어주는 과정은 세 단계라고 공식 문서가 적습니다. 렌더를 트리거하는 단계, 컴포넌트를 렌더하는 단계, DOM 에 커밋하는 단계입니다.

sequenceDiagram
    participant 이벤트 핸들러
    participant React
    participant 컴포넌트
    participant DOM
    이벤트 핸들러->>React: 상태 갱신
    Note over React: 렌더를 큐에 넣는다
    React->>컴포넌트: 함수 호출
    컴포넌트-->>React: 반환한 JSX
    React->>DOM: 달라진 부분만 반영

1단계는 트리거입니다. 컴포넌트가 렌더되는 이유는 둘이라고 문서가 적습니다. 그 컴포넌트의 첫 렌더이거나, 그 컴포넌트나 조상 중 하나의 상태가 갱신됐을 때입니다. 상태를 갱신하면 렌더가 자동으로 큐에 들어갑니다.

2단계는 렌더입니다. 렌더가 트리거되면 React 는 화면에 무엇을 표시할지 알아내려고 컴포넌트를 호출합니다. 문서는 "렌더링이란 React 가 컴포넌트를 호출하는 것" 이라고 적습니다.

3단계는 커밋입니다. 컴포넌트를 호출한 뒤 React 가 DOM 을 수정합니다. 첫 렌더에서는 만들어 둔 DOM 노드를 appendChild() DOM API 로 화면에 붙입니다. 다시 렌더할 때는 렌더 도중에 계산해 둔 최소한의 연산만 적용해 DOM 을 최신 렌더 결과에 맞춥니다. 렌더 사이에 차이가 없으면 React 는 DOM 노드를 건드리지 않습니다.

상태는 스냅샷

상태 변수는 읽고 쓰는 보통의 JavaScript 변수처럼 보입니다. 그러나 문서는 상태가 스냅샷에 더 가깝게 행동한다고 적습니다. 설정해도 이미 가지고 있는 상태 변수가 바뀌지 않습니다. 대신 다시 렌더하게 만듭니다.

문서가 든 순서는 이렇습니다. 버튼을 누르면 onSubmit 이벤트 핸들러가 실행됩니다. setIsSent(true) 가 isSent 를 true 로 만들면서 새 렌더를 큐에 넣습니다. React 는 새 isSent 값에 따라 컴포넌트를 다시 렌더합니다. 컴포넌트라는 함수가 반환한 JSX 는 그 시점 사용자 인터페이스의 스냅샷과 같습니다.

관련 항목

React 를 이루는 구성 요소

컴포넌트 · JSX · props · state · 훅 · 단방향 데이터 흐름 · 순수 컴포넌트 · 조건부 렌더링 · 리스트 렌더링 · 제어 컴포넌트와 비제어 컴포넌트

React가 상태를 다루는 방법

useState · useReducer · Context

React가 여는 비상구

ref · Effect · useEffect · 명령형 API

세밀한 반응성 대신 쓰는 손 최적화

memo · useMemo · React 컴파일러 · 메모이제이션

React에서 로직을 재사용하는 패턴

고차 컴포넌트 · 렌더 프롭 · 커스텀 훅

React가 화면을 갱신하는 내부 동작

렌더와 커밋 · 재조정 · 가상 DOM

React가 제공하는 특수 컴포넌트

StrictMode · Profiler · 에러 경계 · Portal · Fragment

React가 지키는 원칙

선언형 · 순수 함수 · API 안정성

React를 실제로 채택한 조직

Facebook · Twitter · Airbnb

React가 화면을 그리는 대상

DOM · 브라우저 · Node · React Native

React가 규정하지 않아 얹는 프레임워크

Next.js · React Router · 서버 컴포넌트

React가 속하는 상위 분야

JavaScript · 렌더링 · 프론트엔드

다른 이름: react · 리액트