React는 사용자 인터페이스를 구축하기 위한 자바스크립트 라이브러리이다. 페이스북과 개별 개발자 및 기업들로 구성된 커뮤니티에 의해 유지보수된다. React는 주로 단일 페이지 애플리케이션의 개발에 사용되며, 모바일 애플리케이션 개발을 위한 React Native의 기반이 되기도 한다.
React의 핵심은 재사용 가능한 UI 컴포넌트를 조합하여 복잡한 사용자 인터페이스를 구성하는 것이다. 이는 전통적인 템플릿 기반 방식과 달리, 애플리케이션의 상태 변화에 따라 선언적으로 UI가 어떻게 보여야 하는지를 기술하는 방식을 취한다. 결과적으로 개발자는 UI를 갱신하는 구체적인 절차(명령형 프로그래밍)보다는 원하는 UI의 상태(선언형 프로그래밍)에 집중할 수 있다.
React는 2013년 페이스북에 의해 처음 공개되었으며, 빠르게 현대 웹 개발의 핵심 기술 중 하나로 자리 잡았다. 그 인기는 다음과 같은 특징에서 비롯된다.
학습 곡선: 다른 완전한 프레임워크에 비해 비교적 진입 장벽이 낮다.
유연성: 뷰 레이어에 집중하여, 라우팅이나 상태 관리 같은 다른 부분은 개발자가 선호하는 라이브러리와 자유롭게 결합할 수 있다.
생태계: 방대한 서드파티 라이브러리와 도구, 활발한 커뮤니티를 보유하고 있다.
React의 발전은 클래스 컴포넌트 중심에서 함수형 컴포넌트와 React Hooks 중심의 패러다임으로 전환되었다. 2019년에 도입된 Hooks는 함수형 컴포넌트에서도 상태 관리와 생명주기 기능을 사용할 수 있게 하여, 코드의 재사용성과 가독성을 크게 향상시켰다.
React는 사용자 인터페이스를 구축하기 위한 자바스크립트 라이브러리이다. 그 기본 철학은 복잡한 UI를 효율적이고 유지보수 가능하게 만드는 데 있다. 이 철학은 주로 세 가지 핵심 개념, 즉 선언형 UI, 컴포넌트 기반 아키텍처, 그리고 가상 DOM과 재조정 과정을 통해 구현된다.
첫 번째 핵심은 선언형 UI이다. React는 개발자가 UI가 "어떻게" 보여야 하는지를 명령형으로 일일이 지시하는 대신, UI가 "어떤" 상태를 가져야 하는지를 선언적으로 기술하도록 유도한다. 개발자는 특정 상태에 대한 UI의 최종 모습을 JSX로 작성하면, React는 그 설명에 따라 UI를 실제로 구성하고 업데이트한다. 이는 UI 로직을 더욱 예측 가능하고 디버깅하기 쉽게 만든다.
두 번째는 컴포넌트 기반 아키텍처이다. React 애플리케이션은 독립적이고 재사용 가능한 컴포넌트들로 구성된다. 각 컴포넌트는 자신의 로직과 뷰를 캡슐화하며, Props를 통해 데이터를 입력받고, State를 통해 내부 상태를 관리한다. 이 방식은 복잡한 애플리케이션을 작은 단위로 분해하여 개발, 테스트, 재사용을 용이하게 한다.
세 번째는 가상 DOM과 재조정이다. React는 UI의 이상적인 또는 "가상"적인 표현을 메모리에 유지한다. 상태가 변경되면 React는 새로운 가상 DOM 트리를 생성하고 이전 트리와 비교한다. 이 비교 과정을 재조정 또는 Diffing 알고리즘이라고 한다. React는 두 트리 사이의 최소한의 변경 사항만 계산하여 실제 DOM에 효율적으로 적용한다. 이는 직접적인 DOM 조작보다 훨씬 빠른 성능을 제공하는 핵심 메커니즘이다[1].
React는 사용자 인터페이스를 구축하기 위한 JavaScript 라이브러리로, 선언형 프로그래밍 패러다임을 중심에 둔다. 이는 명령형 방식과 대비되는 개념으로, 개발자가 "어떻게(How)" 화면을 업데이트할지에 대한 절차를 일일이 기술하기보다, "무엇(What)"을 화면에 보여주고 싶은지 목표 상태를 선언하는 방식에 가깝다. 예를 들어, 버튼 클릭 시 카운터를 증가시키는 UI를 생각해 볼 수 있다. 명령형 방식에서는 DOM 요소를 직접 선택하고, 이벤트 리스너를 추가하며, 상태 변경 시 요소의 텍스트 콘텐츠를 수동으로 갱신하는 코드를 작성해야 한다. 반면 React의 선언형 방식에서는 count라는 state와, 해당 state를 기반으로 렌더링해야 할 JSX 구조를 정의한다. 상태가 변경되면 React는 새로운 선언(JSX)과 이전 선언을 비교하여 필요한 최소한의 변경만 실제 DOM에 적용한다.
이 접근법의 핵심 이점은 코드의 예측 가능성과 유지보수성이 크게 향상된다는 점이다. UI는 주어진 props와 state에 대한 순수 함수의 결과물로 간주된다. 같은 props와 state가 입력되면 항상 동일한 UI 출력을 보장하므로, 복잡한 상태 변화 속에서도 UI가 어떻게 보일지 추론하기 쉬워진다. 또한, 개발자는 복잡한 DOM 조작과 상태 동기화 로직에서 벗어나, 애플리케이션의 비즈니스 로직과 UI 구조를 설계하는 데 더 집중할 수 있다. 이는 대규모 애플리케이션 개발에서 특히 중요한 장점으로 작용한다.
선언형 UI는 React의 컴포넌트 기반 아키텍처 및 가상 DOM과 밀접하게 연동되어 작동한다. 개발자가 컴포넌트를 통해 UI를 선언하면, React는 이를 내부의 가상 DOM 표현으로 유지한다. 상태가 변경되어 재렌더링이 필요할 때마다, React는 새로운 가상 DOM 트리를 생성하고 이전 트리와 비교하는 재조정 과정을 거친다. 이 비교 알고리즘을 통해 실제로 변경된 부분만 효율적으로 찾아내어 실제 DOM을 업데이트한다. 따라서 개발자는 성능 최적화를 위해 DOM을 직접 조작할 필요 없이, 선언적으로 UI를 작성하는 것만으로도 효율적인 업데이트를 얻을 수 있다[2]. 이 구조는 UI 개발을 보다 직관적이고 생산적으로 만드는 React의 근본적인 철학을 형성한다.
React의 핵심 설계 원칙은 UI를 독립적이고 재사용 가능한 컴포넌트의 조합으로 구성하는 것이다. 각 컴포넌트는 자체적인 로직과 마크업을 캡슐화한 하나의 단위이며, 이들을 조립하여 복잡한 사용자 인터페이스를 구축한다. 이 접근 방식은 코드의 모듈성, 유지보수성, 재사용성을 크게 향상시킨다.
컴포넌트는 함수나 클래스로 정의할 수 있다. 함수 컴포넌트는 Props를 인자로 받아 React 엘리먼트를 반환하는 간단한 함수이다. 클래스 컴포넌트는 React.Component를 상속받아 render 메서드를 구현한다. 컴포넌트는 트리 구조를 형성하며, 상위 컴포넌트는 하위 컴포넌트에 데이터를 Props로 전달한다.
컴포넌트 유형 | 정의 방식 | 주요 특징 |
|---|---|---|
함수 컴포넌트 | JavaScript/TypeScript 함수 | 간결함, React Hooks 사용 가능 |
클래스 컴포넌트 | ES6 클래스 | 생명주기 메서드, 내부 상태(State) 관리 |
이 아키텍처는 개발자가 작은 단위의 컴포넌트를 먼저 만들고, 이를 점차 조합하여 페이지나 애플리케이션을 완성하도록 유도한다. 이는 관심사 분리를 용이하게 하여, 특정 기능이나 UI 부분의 변경이 다른 부분에 미치는 영향을 최소화한다. 결과적으로 대규모 애플리케이션 개발과 팀 협업에 매우 효과적이다.
가상 DOM은 React 애플리케이션의 성능을 최적화하기 위한 핵심 메커니즘이다. 실제 DOM을 직접 조작하는 작업은 비용이 크기 때문에, React는 메모리 상에 가상의 DOM 트리를 구축하고 상태 변화가 있을 때마다 이 가상 DOM을 먼저 업데이트한다. 그 후, 이전 가상 DOM 트리(리액트 엘리먼트 트리)와 새로 생성된 가상 DOM 트리를 비교하는 과정인 재조정을 수행하여 변경된 부분만을 실제 DOM에 효율적으로 반영한다.
재조정 알고리즘은 두 트리를 비교할 때, 루트 엘리먼트부터 시작해 재귀적으로 자식들을 비교한다. 엘리먌트의 타입이 다르면 React는 이전 트리를 완전히 버리고 새로운 트리를 구축한다. 같은 타입의 DOM 엘리먼트라면 변경된 속성만을 업데이트한다. 같은 타입의 컴포넌트 인스턴스라면 해당 인스턴스의 상태를 유지한 채 새 props를 전달하여 컴포넌트의 생명주기 메서드(또는 useEffect 훅)가 호출되도록 한다.
자식 엘리먼트 목록을 효율적으로 처리하기 위해 React는 기본적으로 인덱스를 기준으로 변경 사항을 판단한다. 그러나 이는 리스트의 순서가 바뀌는 경우 비효율적인 업데이트를 초래할 수 있다. 이를 해결하기 위해 개발자는 각 자식 엘리먼트에 고유한 key prop을 제공해야 한다. key를 사용하면 React는 키 값을 통해 각 엘리먼트를 식별하여 이동이 발생한 경우에도 기존 인스턴스와 DOM 노드를 재사용할 수 있다.
비교 상황 | React의 동작 |
|---|---|
엘리먼트 타입이 다른 경우 | 이전 트리를 완전히 제거하고 새 트리를 DOM에 마운트한다. |
같은 타입의 DOM 엘리먼트 | 두 노드를 비교하여 변경된 속성만을 실제 DOM 노드에 패치한다. |
같은 타입의 컴포넌트 엘리먼트 | 컴포넌트 인스턴스를 유지하고, 새 props를 전달하여 리렌더링을 유발한다. |
자식 리렌더링 (key 없음) | 인덱스 비교로 인해 순서 변경 시 비효율적인 업데이트가 발생할 수 있다. |
자식 리렌더링 (key 있음) | 키를 통해 엘리먼트를 식별하여 순서가 바뀌어도 효율적으로 DOM을 재정렬한다. |
이러한 가상 DOM과 재조정 과정은 개발자가 명령형으로 DOM 업데이트를 관리할 필요 없이 선언형 UI를 작성할 수 있게 해주는 기반이 된다. 결과적으로 애플리케이션의 상태 변화에 따른 UI 업데이트가 자동화되고, 불필요한 DOM 조작을 최소화하여 전반적인 성능을 향상시킨다.
JSX는 자바스크립트를 확장한 문법으로, React에서 UI 구조를 기술하는 데 사용된다. HTML과 유사한 문법을 사용하지만, 실제로는 자바스크립트 객체로 변환된다. 이를 통해 자바스크립트 로직과 마크업을 하나의 파일에 함께 작성할 수 있어 가독성과 유지보수성을 높인다. JSX 내부에서는 중괄호({})를 사용하여 자바스크립트 표현식을 포함시킬 수 있다.
Props와 State는 컴포넌트의 데이터 흐름을 결정하는 핵심 개념이다. Props는 부모 컴포넌트로부터 자식 컴포넌트로 전달되는 읽기 전용 데이터이다. 반면 State는 컴포넌트 내부에서 관리되고 변경될 수 있는 데이터이다. State가 변경되면 컴포넌트는 자동으로 다시 렌더링된다. 이 두 가지를 명확히 구분하는 것은 예측 가능한 컴포넌트를 설계하는 데 중요하다.
컴포넌트는 생성부터 소멸까지 일련의 생명주기 단계를 거친다. 주요 생명주기 메서드는 다음과 같다.
단계 | 메서드 | 설명 |
|---|---|---|
마운팅 |
| 컴포넌트가 생성될 때 호출된다. |
마운팅 |
| UI를 렌더링한다. |
마운팅 |
| 컴포넌트가 DOM에 마운트된 직후 호출된다. |
업데이팅 |
| Props나 State가 업데이트된 후 호출된다. |
언마운팅 |
| 컴포넌트가 DOM에서 제거되기 직전 호출된다. |
이러한 생명주기 메서드를 이용해 데이터 가져오기, 구독 설정, 수동 DOM 조작 등의 작업을 적절한 시점에 수행할 수 있다.
React에서 이벤트 처리는 카멜 케이스를 사용한 속성 이름과 함수를 이벤트 핸들러로 전달하는 방식으로 이루어진다. 예를 들어, onClick={handleClick}과 같이 작성한다. 브라우저의 기본 동작을 방지하기 위해 명시적으로 preventDefault()를 호출해야 한다. 이벤트 핸들러 내부에서 State를 업데이트할 경우, 컴포넌트가 다시 렌더링된다.
JSX는 자바스크립트 XML의 약자로, React에서 UI 구조를 기술하기 위해 사용하는 문법 확장이다. 이는 자바스크립트 코드 안에 HTML과 유사한 태그를 직접 작성할 수 있게 하여, 마크업과 로직을 하나의 파일(컴포넌트)에 결합하는 방식을 제공한다. JSX는 React.createElement() 함수 호출을 위한 문법적 설탕 역할을 하며, 최종적으로는 일반 자바스크립트 객체(React 요소)로 변환된다[3].
JSX는 몇 가지 중요한 규칙을 따른다. 하나의 요소는 반드시 단일 최상위 태그로 감싸져야 하며, 태그는 항상 명시적으로 닫혀야 한다. 자바스크립트 표현식을 포함하려면 중괄호({}) 안에 작성해야 한다. 또한, HTML의 class나 for와 같은 예약어 대신 className, htmlFor와 같은 React의 속성명을 사용해야 한다.
JSX를 사용함으로써 개발자는 시각적으로 컴포넌트의 구조를 더 명확히 파악할 수 있고, 에디터의 자동 완성 및 구문 강조 기능의 도움을 받을 수 있다. 아래는 JSX의 간단한 사용 예시와 그 변환 결과를 보여주는 표이다.
JSX 코드 | 변환된 React.createElement 코드 |
|---|---|
|
|
|
|
이 변환 과정은 빌드 시점에 이루어지므로, 런타임 성능에 영향을 주지 않는다. JSX는 React를 사용하는 데 필수는 아니지만, 대부분의 개발자와 공식 문서에서 권장하는 표준적인 방식이다.
Props는 부모 컴포넌트로부터 자식 컴포넌트로 전달되는 읽기 전용 데이터이다. 컴포넌트의 외부에서 주어지며, 컴포넌트 내부에서 직접 수정할 수 없다. Props는 컴포넌트의 구성을 결정하고, 동일한 컴포넌트에 다른 Props를 전달함으로써 다양한 결과를 렌더링할 수 있게 한다. 이를 통해 컴포넌트의 재사용성을 높일 수 있다.
반면 State는 컴포넌트 내부에서 관리되고 변경 가능한 데이터이다. 주로 사용자 상호작용(예: 폼 입력, 버튼 클릭)이나 시간 경과에 따라 변해야 하는 컴포넌트의 상태를 나타낸다. State가 변경되면 React는 해당 컴포넌트와 필요한 하위 컴포넌트들을 자동으로 다시 렌더링하여 UI를 업데이트한다.
Props와 State의 주요 차이점은 다음과 같다.
구분 | Props | State |
|---|---|---|
소유권 | 부모 컴포넌트가 소유 | 컴포넌트 자체가 소유 |
변경 가능성 | 읽기 전용(불변성) | 변경 가능 |
데이터 흐름 | 상위에서 하위로 단방향 | 컴포넌트 내부에서 관리 |
목적 | 컴포넌트 구성 및 초기값 설정 | 컴포넌트의 동적 상태 관리 |
일반적으로 컴포넌트는 자신의 State를 하위 컴포넌트의 Props로 전달할 수 있다. 이는 "단방향 데이터 흐름"의 원칙을 따르며, 애플리케이션의 상태 변화를 예측 가능하게 만드는 데 기여한다. State는 가능한 한 최소화하고, 필요한 경우에만 사용하는 것이 좋다. 여러 컴포넌트가 공유해야 하는 상태는 상태 끌어올리기 패턴을 사용하거나 Context API 또는 외부 상태 관리 라이브러리를 활용하여 관리한다.
컴포넌트 생명주기는 React 컴포넌트가 생성되고, 업데이트되며, 제거되는 일련의 과정을 의미한다. 각 단계에서 특정 메서드가 호출되어 개발자가 컴포넌트의 동작을 세밀하게 제어할 수 있다. 이 개념은 주로 클래스형 컴포넌트에서 명시적으로 사용되었으나, 함수형 컴포넌트와 React Hooks의 등장 이후에는 훅을 통해 생명주기의 핵심 개념을 다르게 구현한다.
클래스형 컴포넌트의 생명주기는 크게 마운트, 업데이트, 언마운트 세 단계로 나뉜다. 마운트 단계에서는 컴포넌트가 DOM에 처음 삽입될 때 constructor, render, componentDidMount 메서드가 순서대로 호출된다. componentDidMount는 외부 데이터를 가져오거나 구독을 설정하는 데 주로 사용된다. 업데이트 단계는 props나 state가 변경될 때 발생하며, shouldComponentUpdate, render, componentDidUpdate 등의 메서드가 호출된다. 언마운트 단계에서는 컴포넌트가 DOM에서 제거되기 직전에 componentWillUnmount 메서드가 호출되어 구독 해제나 정리 작업을 수행한다.
함수형 컴포넌트에서는 useEffect 훅이 생명주기 메서드의 역할을 대체한다. useEffect의 의존성 배열을 조정하여 마운트/언마운트 시점과 특정 값이 변경될 때의 사이드 이펙트를 관리할 수 있다. 예를 들어, 빈 의존성 배열([])을 전달하면 componentDidMount와 componentWillUnmount의 동작을, 배열에 특정 값을 넣으면 componentDidUpdate의 동작을 모방한다.
생명주기 단계 | 클래스형 컴포넌트 메서드 | 함수형 컴포넌트 대응 훅 |
|---|---|---|
마운트 |
|
|
업데이트 |
|
|
언마운트 |
|
|
현대 React 개발에서는 함수형 컴포넌트와 훅을 사용하는 것이 권장되며, 이에 따라 생명주기 관리의 패러다임도 "라이프사이클 메서드 실행"에서 "사이드 이펙트와 의존성 관리"로 전환되었다.
React에서 이벤트 처리는 HTML DOM의 이벤트 처리와 유사하지만 몇 가지 문법적 차이를 가진다. React 이벤트는 소문자 대신 카멜 케이스를 사용하여 명명한다. 예를 들어, onclick 대신 onClick을, onsubmit 대신 onSubmit을 사용한다. 또한, JSX를 사용할 때 이벤트 핸들러로 문자열이 아닌 함수를 전달한다. 기본 동작을 방지하기 위해 false를 반환하는 대신, 명시적으로 preventDefault 메서드를 호출해야 한다.
이벤트 핸들러 함수는 일반적으로 컴포넌트 내부에 정의되며, state를 업데이트하는 로직을 포함한다. 핸들러에 추가적인 매개변수를 전달해야 할 경우, 화살표 함수나 bind 메서드를 사용한다. React의 이벤트 객체는 합성 이벤트라 불리며, 모든 브라우저에서 동일한 인터페이스를 제공하도록 W3C 표준을 따르도록 설계되었다. 이 객체는 이벤트 풀링을 사용하여 성능을 최적화하므로, 비동기적으로 이벤트 객체에 접근해야 한다면 event.persist() 메서드를 호출해야 한다.
주요 이벤트와 React에서의 속성명은 아래 표와 같다.
DOM 이벤트 | React 속성명 |
|---|---|
onclick | onClick |
onchange | onChange |
onsubmit | onSubmit |
onkeydown | onKeyDown |
onmouseover | onMouseOver |
React Hooks는 React 16.8 버전에서 도입된 기능으로, 클래스 컴포넌트를 작성하지 않고도 상태와 다른 React의 기능들을 사용할 수 있게 해준다. 이는 함수형 프로그래밍 패러다임을 더욱 강화하며, 로직의 재사용과 컴포넌트의 관심사 분리를 용이하게 한다. Hooks의 등장으로 인해 함수 컴포넌트는 내부 상태 관리, 생명주기 연동, 컨텍스트 사용 등 이전에는 클래스에서만 가능했던 모든 기능을 수행할 수 있는 완전한 컴포넌트가 되었다.
기본 Hooks인 useState와 useEffect는 가장 빈번하게 사용된다. useState는 함수 컴포넌트 내부에 지역적인 상태를 추가할 수 있게 하며, 상태 값과 그 값을 갱신하는 함수를 쌍으로 반환한다. useEffect는 부수 효과를 수행할 수 있게 하여, 데이터 가져오기, 구독 설정, DOM 수동 조작 등 생명주기 메서드(componentDidMount, componentDidUpdate, componentWillUnmount)의 역할을 대체한다. 의존성 배열을 통해 효과가 실행되는 조건을 제어할 수 있다.
추가 Hooks는 더 특화된 상황을 처리한다. useContext는 컴포넌트 트리를 통해 데이터를 명시적으로 props로 전달하지 않고도 구독할 수 있게 한다. useReducer는 useState의 대체제로, 복잡한 상태 로직을 관리할 때 유용하다. useCallback과 useMemo는 성능 최적화를 위한 메모이제이션 Hook으로, 불필요한 재생성과 재계산을 방지한다.
Hook 이름 | 주요 용도 |
|---|---|
함수 컴포넌트에 상태 변수 추가 | |
부수 효과 수행 및 생명주기 연동 | |
Context 객체의 현재 값을 읽고 구독 | |
복잡한 상태 로직을 리듀서로 관리 | |
함수를 메모이제이션하여 불필요한 재생성 방지 | |
비용이 큰 계산 결과를 메모이제이션 |
개발자는 하나 이상의 기본 Hook을 조합하여 자신만의 커스텀 Hook을 만들 수 있다. 커스텀 Hook은 상태 관련 로직을 컴포넌트로부터 분리해 재사용 가능한 함수로 만드는 메커니즘이다. 이를 통해 여러 컴포넌트에서 동일한 로직을 공유하면서도 각 컴포넌트의 상태는 완전히 독립적으로 유지된다. 모든 Hook은 함수 컴포넌트의 최상위 레벨에서만 호출해야 하며, 반복문, 조건문, 중첩 함수 내에서는 호출할 수 없다는 규칙을 따른다.
useState는 함수형 컴포넌트 내부에 상태를 추가할 수 있게 해주는 React Hooks이다. 이 훅은 상태 변수와 해당 변수를 갱신하는 함수를 쌍으로 반환한다. 초기 상태는 함수의 인자로 설정하며, 상태 갱신 함수를 호출하면 컴포넌트는 새로운 상태로 다시 렌더링된다. 클래스 컴포넌트의 this.setState와 달리, useState로 갱신된 상태는 새로운 객체로 완전히 대체되지 않고, 제공된 새 값으로 직접 대체된다[4].
useEffect는 함수형 컴포넌트에서 부수 효과를 수행할 수 있게 한다. 이 훅은 두 개의 인자를 받는데, 첫 번째는 실행할 효과가 담긴 함수(효과 함수)이고, 두 번째는 선택적인 의존성 배열이다. 의존성 배열이 비어 있으면 컴포넌트 마운트 시에만 효과 함수가 실행되며, 배열에 값이 있으면 해당 값이 변경될 때마다 실행된다. 배열을 생략하면 모든 렌더링 후에 효과 함수가 실행된다. 클래스 컴포넌트의 componentDidMount, componentDidUpdate, componentWillUnmount 생명주기 메서드의 기능을 하나의 API로 통합한 개념이다.
두 훅은 함께 사용되는 경우가 많다. 예를 들어, 컴포넌트가 마운트될 때 API에서 데이터를 가져오고(useEffect), 그 데이터를 컴포넌트 내부 상태에 저장하여(useState) UI를 갱신하는 패턴이 대표적이다. useEffect의 효과 함수는 함수를 반환할 수 있는데, 이 반환된 함수는 컴포넌트가 언마운트되거나 다음 효과가 실행되기 전에 정리 작업을 수행하는 데 사용된다[5].
useContext는 React 컴포넌트 트리 전역에서 데이터를 공유할 수 있게 해주는 Hook이다. Context 객체를 생성하고 Provider로 값을 제공하면, 하위 모든 컴포넌트에서 useContext를 호출하여 해당 값을 직접 읽을 수 있다. 이는 Props를 계층 깊이 전달하는 "prop drilling" 문제를 해결하는 데 유용하다. 주로 테마, 사용자 인증 정보, 언어 설정과 같은 전역 상태를 관리할 때 사용된다.
useReducer는 useState의 대체제로, 더 복잡한 상태 로직을 관리할 수 있다. (state, action) => newState 형태의 리듀서(reducer) 함수와 초기 상태를 받아 현재 상태와 dispatch 함수를 반환한다. 상태 업데이트 로직을 컴포넌트 외부로 분리할 수 있으며, 여러 하위 값이 복잡하게 연관되어 있거나 다음 상태가 이전 상태에 의존적인 경우에 적합하다. Redux 패턴과 유사한 방식으로 상태를 관리할 수 있다.
useCallback과 useMemo는 성능 최적화를 위한 Hook이다. useCallback은 메모이제이션된 콜백 함수를 반환한다. 의존성 배열이 변경되지 않으면 동일한 함수 참조를 유지하여, 불필요한 자식 컴포넌트의 리렌더링을 방지한다. 주로 자식 컴포넌트에 Props로 함수를 전달할 때 사용된다. useMemo는 메모이제이션된 *값*을 반환한다. 복잡한 계산 함수와 의존성 배열을 받아, 의존성이 변경될 때만 함수를 재실행하여 계산된 값을 다시 구한다. 비용이 큰 연산 결과를 캐시하는 데 사용된다.
Hook | 주요 목적 | 반환 값 | 최적화 대상 |
|---|---|---|---|
전역 상태/값 읽기 | Context가 제공하는 현재 값 | - | |
복잡한 상태 로직 관리 | 현재 상태와 | 상태 업데이트 로직 | |
함수 참조 동일성 유지 | 메모이제이션된 콜백 함수 | 함수 | |
계산 비용이 큰 값 캐싱 | 메모이제이션된 계산 값 | 값 |
이러한 추가 Hook들은 애플리케이션의 상태 흐름을 더 체계적으로 구성하고, 불필요한 렌더링과 계산을 줄여 성능을 개선하는 데 기여한다.
커스텀 훅은 React Hooks를 조합하여 자신만의 로직을 포함하는 재사용 가능한 함수를 만드는 것을 말한다. 이는 컴포넌트 로직을 함수로 추출하여 여러 컴포넌트 간에 상태 관련 동작을 공유할 수 있게 해준다. 커스텀 훅의 이름은 반드시 use로 시작해야 하며, 내부에서 다른 훅을 호출할 수 있다. 이는 React가 훅의 호출 순서에 의존하기 때문에 필요한 규칙이다.
커스텀 훅을 만드는 주요 동기는 로직의 재사용성과 컴포넌트 코드의 단순화이다. 예를 들어, 폼 입력값 관리, API 데이터 페칭, 구독 설정, 로컬 스토리지와의 상호작용 등 반복되는 패턴을 하나의 함수로 캡슐화할 수 있다. 이렇게 추출된 로직은 상태와 부수 효과를 그대로 유지하면서도 여러 컴포넌트에서 독립적으로 사용될 수 있다.
다음은 로컬 스토리지와 상태를 동기화하는 간단한 커스텀 훅의 예시이다.
```javascript
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error(error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
```
이 훅은 useState와 useEffect를 사용하여 구현되었으며, 컴포넌트에서는 const [name, setName] = useLocalStorage('username', '');과 같이 사용할 수 있다. 커스텀 훅을 설계할 때는 단일 책임 원칙을 지키고, 명확한 인터페이스(매개변수와 반환값)를 제공하는 것이 좋다. 잘 설계된 커스텀 훅은 애플리케이션 전반의 코드 중복을 줄이고 유지보수성을 크게 향상시킨다.
컴포넌트 설계 패턴은 React 애플리케이션의 코드 구조를 체계화하고 재사용성, 유지보수성, 테스트 용이성을 높이기 위한 방법론이다. 주로 사용되는 패턴으로는 컨테이너 컴포넌트와 프레젠테이션 컴포넌트의 분리, 고차 컴포넌트, 렌더 프롭 등이 있다.
컨테이너 컴포넌트와 프레젠테이션 컴포넌트 패턴은 관심사를 분리하는 데 중점을 둔다. 컨테이너 컴포넌트는 주로 데이터를 가져오고 상태를 관리하며 비즈니스 로직을 처리하는 역할을 담당한다. 반면, 프레젠테이션 컴포넌트는 Props를 통해 전달받은 데이터를 시각적으로 표현하는 데 집중하며, 내부 상태를 거의 가지지 않는다. 이 분리는 컴포넌트의 재사용성을 극대화하고, UI를 독립적으로 테스트하기 쉽게 만든다.
고차 컴포넌트는 컴포넌트를 인자로 받아 새로운 기능을 추가한 컴포넌트를 반환하는 함수이다. 주로 로직의 재사용을 위해 사용되며, 인증 확인, 데이터 구독, 스타일 적용 등 여러 컴포넌트에 공통적으로 필요한 기능을 캡슐화한다. HOC는 컴포넌트를 감싸는 형태로 동작하여 기존 컴포넌트의 동작을 변경하거나 확장한다.
렌더 프롭 패턴은 컴포넌트의 자식으로 함수를 전달하여, 해당 함수 내부에서 컴포넌트의 상태나 로직을 인자로 받아 렌더링을 위임하는 방식이다. 이 패턴은 컴포넌트의 렌더링 결과를 동적으로 결정해야 할 때 유용하며, HOC와 유사한 로직 재사용 목적을 가지지만, 구성이 더 명시적이다. 주로 마우스 추적, 데이터 페칭 등 공통 기능을 제공하는 컴포넌트에서 활용된다.
패턴 | 주요 목적 | 구현 방식 | 특징 |
|---|---|---|---|
컨테이너/프레젠테이션 | 관심사 분리 | 로직 컴포넌트와 UI 컴포넌트 분리 | 재사용성과 테스트 용이성 향상 |
고차 컴포넌트(HOC) | 로직 재사용 | 컴포넌트를 감싸는 함수 | 다중 감싸기 가능, 추상화 제공 |
렌더 프롭 | 렌더링 제어 위임 | 자식으로 함수를 Props로 전달 | 구성이 명시적, 렌더링 유연성 높음 |
컨테이너와 프레젠테이션 컴포넌트 패턴은 React 애플리케이션의 관심사를 분리하기 위한 설계 패턴이다. 이 패턴은 UI 로직과 비즈니스 로직을 분리하여 컴포넌트의 재사용성과 테스트 용이성을 높이는 것을 목표로 한다. 컨테이너 컴포넌트는 데이터를 가져오고 상태를 관리하는 '어떻게 동작하는가'에 집중하며, 프레젠테이션 컴포넌트는 데이터를 시각적으로 표시하는 '어떻게 보이는가'에 집중한다.
프레젠테이션 컴포넌트는 주로 props를 통해 데이터를 전달받아 렌더링만을 담당하는 순수 함수 형태의 컴포넌트이다. 이 컴포넌트는 내부 상태를 거의 가지지 않으며, UI와 스타일링에 관한 책임만을 가진다. 반면, 컨테이너 컴포넌트는 state를 관리하고, useEffect 훅을 사용해 데이터를 가져오며, 비즈니스 로직을 처리한다. 그 후 처리된 데이터를 프레젠테이션 컴포넌트에 props로 전달한다.
이 패턴의 장점과 단점은 다음과 같이 정리할 수 있다.
장점 | 단점 |
|---|---|
관심사의 명확한 분리로 코드 유지보수성 향상 | 보일러플레이트 코드가 증가할 수 있음 |
프레젠테이션 컴포넌트의 높은 재사용성 | React Hooks의 등장으로 패턴의 필요성이 다소 감소[6] |
비즈니스 로직과 UI 로직의 독립적 테스트 가능 | 과도한 분리는 컴포넌트 트리를 복잡하게 만들 수 있음 |
이 패턴은 React Hooks가 도입되기 전에 널리 사용되었으며, 여전히 대규모 애플리케이션에서 구조를 명확히 하는 데 유용한 접근법으로 남아 있다. 현대적인 React 개발에서는 이 패턴을 엄격하게 따르기보다는, 커스텀 훅을 활용해 로직을 분리하면서도 컴포넌트 구조를 더 유연하게 구성하는 경향이 있다.
고차 컴포넌트(Higher-Order Component, HOC)는 React에서 컴포넌트 로직을 재사용하기 위한 고급 기술이다. 다른 컴포넌트를 받아서 새로운 기능이 추가된 컴포넌트를 반환하는 함수를 의미한다. 이는 함수형 프로그래밍의 고차 함수 개념에서 차용한 패턴으로, 컴포넌트를 인자로 받아 조작하거나 감싸는 방식으로 동작한다.
HOC의 일반적인 구조는 다음과 같다.
```javascript
const EnhancedComponent = higherOrderComponent(WrappedComponent);
```
여기서 higherOrderComponent는 함수이며, WrappedComponent는 기존 컴포넌트, EnhancedComponent는 기능이 강화된 새 컴포넌트이다. HOC는 주로 공통적인 관심사(Cross-Cutting Concerns)를 분리하는 데 사용된다. 예를 들어, 인증 상태 확인, 데이터 가져오기, 스타일 적용, 또는 특정 Props 주입 등의 로직을 여러 컴포넌트에 중복 작성하지 않고 HOC 하나로 캡슐화할 수 있다.
HOC를 구현할 때는 몇 가지 주의사항이 있다. 입력된 컴포넌트의 Props를 무단으로 수정하거나 억제해서는 안 되며, 새로운 Props를 통해 기능을 추가해야 한다. 또한, 렌더링 최적화를 위해 정적 메서드는 따로 복사해야 하며, Ref는 전달되지 않으므로 React.forwardRef를 함께 사용해야 할 수 있다. HOC는 렌더 메서드 내부에서 사용되지 않도록 해야 성능 저하와 상태 유실을 방지할 수 있다.
이 패턴은 React의 컴포넌트 기반 아키텍처를 활용한 강력한 재사용 메커니즘이었으나, React Hooks의 등장 이후 동일한 로직을 더 간결하고 직관적으로 구현할 수 있게 되었다. 따라서 새 코드에서는 커스텀 Hooks를 우선적으로 고려하는 것이 일반적이다. 그러나 기존의 HOC 기반 라이브러리(예: connect from react-redux)를 사용하거나 클래스 컴포넌트를 다뤄야 하는 레거시 프로젝트에서는 여전히 유용하게 활용된다.
렌더 프롭은 React에서 컴포넌트 간의 로직과 코드 재사용을 가능하게 하는 고급 패턴 중 하나이다. 이 패턴은 컴포넌트가 자식 컴포넌트에게 콜백 함수 형태의 Props를 제공하고, 그 자식 컴포넌트가 해당 함수를 호출하여 JSX를 렌더링하도록 하는 방식을 사용한다. 이름 그대로 '렌더링을 위한 Prop'이라는 의미를 지닌다.
구현 방식은, 로직을 포함하는 컴포넌트(공급자)가 render라는 이름의 Prop이나 다른 이름의 함수 Prop을 자식 컴포넌트에게 전달하는 것이다. 자식 컴포넌트는 이 함수를 호출하며, 필요한 데이터를 인자로 넘겨주고, 함수는 그 데이터를 사용해 최종적으로 렌더링할 UI 요소를 반환한다. 이는 고차 컴포넌트(HOC)와 유사한 목적을 가지지만, 상속이 아닌 합성을 통해 구현된다는 점에서 차이가 있다.
렌더 프롭 패턴의 주요 장점과 활용 사례는 다음과 같다.
장점 | 설명 |
|---|---|
유연성 | 렌더링될 UI의 구체적인 형태를 부모 컴포넌트가 완전히 제어할 수 있어, 동일한 로직으로 다양한 시각적 표현이 가능하다. |
합성 중심 | 컴포넌트 계층 구조를 깊게 만들지 않고도 로직을 공유할 수 있으며, Wrapper Hell 문제를 완화할 수 있다. |
명시적 데이터 흐름 | 어떤 데이터가 자식 컴포넌트로 전달되는지 Props를 통해 명확하게 확인할 수 있다. |
일반적으로 마우스 추적, 데이터 fetching, 폼 상태 관리 등 비즈니스 로직과 시각적 표현을 분리해야 할 때 유용하게 적용된다. 예를 들어, 데이터를 가져오는 로직을 가진 <DataFetcher> 컴포넌트는 render Prop을 통해 data와 loading 상태를 자식에게 전달하고, 자식은 이를 이용해 조건부 렌더링을 수행할 수 있다. 이후 React Hooks의 등장으로 useState, useEffect 등을 활용한 커스텀 훅이 같은 문제를 해결하는 더 간결한 방법으로 널리 사용되면서, 렌더 프롭 패턴의 사용 빈도는 다소 줄어들었다. 그러나 여전히 클래스 컴포넌트 환경이나 특정 설계 요구사항에 따라 유효한 패턴으로 남아 있다.
상태 관리는 React 애플리케이션에서 컴포넌트 간에 데이터를 공유하고 동기화하는 방법을 다루는 중요한 주제이다. 애플리케이션이 커짐에 따라 Props를 통한 단방향 데이터 흐름만으로는 상태를 효율적으로 관리하기 어려워지며, 이를 해결하기 위한 다양한 방법이 존재한다.
Context API는 React 자체에 내장된 상태 공유 메커니즘이다. createContext 함수로 생성된 Context 객체는 Provider 컴포넌트를 통해 하위 트리에 값을 제공하고, useContext 훅을 사용하여 해당 값을 구독한다. 이는 주로 테마, 사용자 인증 정보, 언어 설정 등 전역적으로 필요한 데이터를 전달하는 데 적합하다. 그러나 Context의 값이 변경되면 해당 Provider의 모든 자식 컴포넌트가 리렌더링될 수 있어, 성능에 주의를 기울여야 한다.
복잡한 애플리케이션에서는 Redux, MobX, Recoil, Zustand와 같은 외부 상태 관리 라이브러리를 연동하는 것이 일반적이다. 이들 라이브러리는 중앙 집중식 스토어를 통해 상태를 관리하고, 상태 변경을 예측 가능한 방식으로 처리한다. 예를 들어, Redux는 액션, 리듀서, 디스패치의 패턴을 따르며, React-Redux 라이브러리를 통해 React 컴포넌트와 연결된다. 이러한 라이브러리들은 디버깅 도구, 미들웨어, 비동기 액션 처리 등 추가 기능을 제공하여 대규모 애플리케이션의 상태 관리 복잡성을 줄여준다.
상태 관리 전략을 선택할 때는 애플리케이션의 규모, 팀의 익숙함, 상태 업데이트의 빈도와 복잡성을 고려해야 한다. 간단한 상태 공유에는 Context API와 useReducer 훅의 조합으로 충분할 수 있지만, 고도로 구조화된 상태 로직이 필요하다면 전문 라이브러리 도입이 유리하다.
Context API는 React 애플리케이션에서 컴포넌트 트리를 통해 데이터를 명시적으로 props로 전달하지 않고도 공유할 수 있게 해주는 기능이다. 주로 전역적(global)으로 필요한 데이터, 예를 들어 사용자 인증 정보, 테마, 선호 언어 등을 여러 컴포넌트에 효율적으로 전달하는 데 사용된다.
Context는 React.createContext()로 생성한다. 이 함수는 Provider와 Consumer 컴포넌트를 포함하는 객체를 반환한다. 데이터를 제공하는 상위 컴포넌트에서는 <MyContext.Provider value={someValue}>로 자식 컴포넌트들을 감싼다. 그러면 하위의 모든 컴포넌트는 해당 Context를 구독하고 value prop의 변화를 감지하여 리렌더링된다. 함수형 컴포넌트에서는 useContext Hook을 사용하여 Context 값을 간편하게 읽을 수 있다.
Context API는 주로 변하지 않는 정적인 값보다는 자주 변경될 수 있는 동적인 값을 전파하는 데 적합하다. 그러나 모든 상태 관리를 Context에만 의존하면 컴포넌트 재사용성이 떨어지고 성능 문제가 발생할 수 있다[7]. 따라서 상태의 범위를 적절히 분리하고, 자주 변경되는 복잡한 상태는 Redux나 MobX 같은 외부 상태 관리 라이브러리와 조합하여 사용하는 것이 일반적이다.
React 애플리케이션의 복잡도가 증가하면, 컴포넌트 간에 공유해야 하는 상태가 많아지고 그 흐름이 복잡해질 수 있다. Context API는 이러한 전역 상태 관리에 유용하지만, 대규모 애플리케이션에서는 성능 최적화나 미들웨어, 개발자 도구 지원 등의 면에서 한계가 있을 수 있다. 이때 Redux, MobX, Recoil, Zustand와 같은 외부 상태 관리 라이브러리를 연동하여 사용하는 것이 일반적이다.
이러한 라이브러리들은 각각 고유한 철학과 패턴을 가지고 있다. 예를 들어, Redux는 단일 스토어, 불변성, 순수 함수 형태의 리듀서를 통한 상태 업데이트를 강조하는 플럭스 아키텍처를 따른다. 반면 MobX는 관찰 가능한 상태와 리액션을 활용한 반응형 프로그래밍 모델을 제공한다. 라이브러리 선택은 프로젝트의 규모, 팀의 익숙함, 필요한 기능(예: 비동기 액션 처리, 지속성, 개발 도구)에 따라 결정된다.
주요 외부 상태 관리 라이브러리와 React 연동 방식은 다음과 같다.
라이브러리 | 주요 특징 | React 연동 패키지 |
|---|---|---|
예측 가능한 상태 컨테이너, 불변성, 미들웨어 지원 |
| |
반응형 상태 관리, 관찰 가능한 상태와 액션 |
| |
| ||
간결한 API, Context Provider 불필요, 불변성 유지 도구 내장 |
|
이들 라이브러리를 React 프로젝트에 통합할 때는 주로 해당 라이브러리의 React 바인딩 패키지를 설치하고, 상태 스토어를 생성한 후, 훅 또는 고차 컴포넌트를 통해 컴포넌트에서 상태를 구독하고 업데이트한다. 예를 들어, react-redux는 Provider 컴포넌트로 앱을 감싸고, useSelector, useDispatch 훅을 제공한다. 이러한 연동을 통해 컴포넌트는 비즈니스 로직과 상태 관리 로직을 분리하면서도 효율적으로 전역 상태에 접근하고 업데이트할 수 있다.
성능 최적화는 대규모 React 애플리케이션에서 사용자 경험과 효율성을 보장하는 핵심 과제이다. 주요 접근 방식은 불필요한 렌더링을 줄이고, 애플리케이션 번들의 초기 로딩 시간을 단축하며, 메모리 사용을 효율적으로 관리하는 데 초점을 맞춘다.
렌더링 최적화의 첫 번째 단계는 컴포넌트의 불필요한 재렌더링을 방지하는 것이다. React.memo는 Props가 변경되지 않았을 때 컴포넌트의 재렌더링을 방지하는 고차 컴포넌트이다. useCallback과 useMemo 훅은 각각 함수와 계산된 값을 메모이제이션하여, 의존성 배열이 변경되지 않는 한 동일한 참조값을 유지하도록 한다. 이는 자식 컴포넌트에 함수나 객체를 Props로 전달할 때 유용하게 활용된다. 클래스형 컴포넌트에서는 shouldComponentUpdate 생명주기 메서드나 React.PureComponent를 사용하여 유사한 최적화를 수행할 수 있다.
초기 로딩 성능을 개선하기 위해 코드 스플리팅 기법이 널리 사용된다. 이는 애플리케이션 번들을 여러 청크로 분할하여, 사용자가 현재 필요로 하는 코드만 초기에 로드하도록 한다. React.lazy와 Suspense 컴포넌트를 조합하면 컴포넌트 단위의 지연 로딩을 쉽게 구현할 수 있다. 또한, 웹팩이나 Vite 같은 번들러가 제공하는 동적 import 문법을 활용하여 라우트 기반 코드 분할을 적용하는 것이 일반적이다. 가상 목록(Virtualized List) 라이브러리를 사용하면 수천 개의 리스트 아이템을 효율적으로 렌더링할 수 있다[8].
최적화 기법 | 목적 | 사용 훅/API |
|---|---|---|
컴포넌트 메모이제이션 | 불필요한 재렌더링 방지 |
|
함수/값 메모이제이션 | 의존성 변경 시에만 새 참조 생성 |
|
코드 분할 | 초기 번들 크기 감소, 로딩 속도 향상 |
|
가상화 | 대량 데이터 렌더링 성능 개선 |
|
성능 문제를 식별하기 위해서는 React Developer Tools의 프로파일러 기능을 활용하여 컴포넌트 렌더링 시간과 빈도를 측정할 수 있다. 렌더링 과정에서 무거운 계산이 수행된다면, 해당 계산을 useMemo로 감싸거나 웹 워커로 분리하는 것을 고려해야 한다. 상태 업데이트의 배칭을 이해하고, 불필요하게 상태를 분산시키지 않는 것도 전체적인 렌더링 효율에 기여한다.
메모이제이션은 동일한 계산을 반복하지 않도록 이전에 계산한 결과를 저장해 두고, 동일한 입력이 들어왔을 때 저장된 결과를 재사용하는 성능 최적화 기법이다. React 애플리케이션에서 불필요한 리렌더링과 무거운 계산을 방지하는 데 핵심적인 역할을 한다.
React는 React.memo, useMemo, useCallback과 같은 API를 제공하여 메모이제이션을 구현한다. React.memo는 고차 컴포넌트로, Props가 변경되지 않았다면 컴포넌트의 리렌더링을 방지한다. useMemo 훅은 의존성 배열이 변경되지 않는 한, 비용이 큰 계산의 결과를 캐시한다. useCallback 훅은 함수 참조를 메모이제이션하여, 자식 컴포넌트에 함수를 Props로 전달할 때 불필요한 리렌더링을 유발하지 않도록 한다.
적절한 메모이제이션 전략을 수립하기 위해서는 성능 병목 지점을 먼저 식별하는 것이 중요하다. 모든 컴포넌트와 값에 메모이제이션을 적용하면 오히려 메모리 사용량이 증가하고 비교 연산의 오버헤드가 발생할 수 있다[9]. 일반적으로 렌더링이 빈번한 부모 컴포넌트의 자식 컴포넌트, 큰 배열을 처리하거나 복잡한 연산을 수행하는 함수, 그리고 의존성 배열에 포함된 값이 자주 변경되지 않는 경우에 적용하는 것이 효과적이다.
훅/API | 메모이제이션 대상 | 주요 사용 사례 |
|---|---|---|
| 함수형 컴포넌트의 렌더링 결과 | Props가 동일할 때 리렌더링 스킵 |
| 계산된 값 | 필터링/정렬된 리스트, 무거운 연산 결과 |
| 함수 참조 | 자식 컴포넌트에 전달되는 이벤트 핸들러 |
코드 스플리팅은 애플리케이션의 초기 번들 크기를 줄여 로딩 성능을 향상시키는 기법이다. 애플리케이션의 모든 코드를 하나의 큰 파일로 번들링하는 대신, 필요에 따라 비동기적으로 로드될 수 있는 여러 개의 작은 청크로 분할한다. 이는 특히 대규모 단일 페이지 애플리케이션에서 초기 진입 장벽을 낮추고 사용자 경험을 개선하는 데 핵심적인 역할을 한다.
React에서는 주로 두 가지 방식으로 코드 스플리팅을 구현한다. 첫 번째는 React.lazy와 Suspense 컴포넌트를 사용하는 방법이다. React.lazy 함수는 동적으로 임포트되는 컴포넌트를 렌더링할 수 있게 해주며, Suspense 컴포넌트는 로딩이 완료될 때까지 대체 UI(예: 로딩 스피너)를 보여준다. 두 번째 방식은 웹팩과 같은 번들러가 제공하는 동적 import() 문법을 직접 사용하는 것이다. 이는 라우트 기반 스플리팅이나 컴포넌트 내 특정 조건에서의 스플리팅에 유연하게 적용될 수 있다.
적용 전략은 일반적으로 라우트 단위와 컴포넌트 단위로 나뉜다. 라우트 기반 스플리팅은 각 페이지나 주요 기능 모듈을 별도의 청크로 분리하는 가장 일반적인 방법이다. 컴포넌트 단위 스플리팅은 모달, 헤비한 차트 라이브러리, 사용 빈도가 낮은 UI 섹션 등 초기 렌더링에 필수적이지 않은 부분을 지연 로딩할 때 사용된다. 효과적인 스플리팅을 위해서는 번들 분석 도구를 사용해 번들 크기를 모니터링하고, 어떤 모듈이 가장 큰 부담을 주는지 식별하는 과정이 필요하다[10].
렌더링 최적화는 React 애플리케이션의 반응성과 효율성을 높이는 핵심적인 과정이다. 불필요한 리렌더링을 최소화하고, 실제 DOM 업데이트를 필요한 부분에만 국한시키는 것이 주요 목표이다. 이를 위해 React는 가상 DOM과 재조정 알고리즘을 제공하지만, 개발자가 컴포넌트 설계와 상태 관리에 주의를 기울여야 최상의 성능을 얻을 수 있다.
가장 일반적인 최적화 기법은 React.memo를 사용한 컴포넌트 메모이제이션이다. 이는 컴포넌트가 동일한 props를 받을 때 불필요한 리렌더링을 방지한다. 클래스 컴포넌트의 경우 shouldComponentUpdate 생명주기 메서드나 PureComponent를 사용하여 유사한 효과를 얻을 수 있다. 또한, useCallback과 useMemo 훅을 활용하여 함수 참조나 복잡한 계산 결과를 메모이제이션하면, 자식 컴포넌트에 전달되는 props의 불필요한 변경을 막을 수 있다.
컴포넌트를 작고 독립적인 단위로 분리하는 것도 중요하다. 상태 변화가 빈번한 컴포넌트와 정적인 컴포넌트를 분리하면, 상태 변화의 영향을 받는 범위를 국소화할 수 있다. 또한, 리스트를 렌더링할 때는 각 항목에 고유한 key prop을 제공해야 하며, 가능하면 인덱스보다는 항목의 고유 ID를 사용하는 것이 좋다. 이는 React가 항목의 추가, 삭제, 재정렬을 효율적으로 추적하는 데 도움을 준다.
최적화 기법 | 사용 목적 | 주요 도구/방법 |
|---|---|---|
컴포넌트 메모이제이션 | props가 변경되지 않았을 때 리렌더링 방지 |
|
값/함수 메모이제이션 | 자식 컴포넌트에 전달되는 props의 불필요한 생성 방지 |
|
컴포넌트 분리 | 상태 변경의 영향을 받는 범위 최소화 | 상태 로직과 프레젠테이션 로직 분리 |
효율적인 리스트 렌더링 | 리스트 항목의 변경 사항 효율적 추적 | 고유하고 안정적인 |
마지막으로, React DevTools의 Profiler 기능을 사용하여 렌더링 성능을 측정하고 병목 현상을 찾는 것이 좋다. 이를 통해 어떤 컴포넌트가, 왜, 얼마나 자주 리렌더링되는지 분석할 수 있다. 모든 컴포넌트에 최적화를 적용하는 것은 오히려 성능을 저하시킬 수 있으므로, 실제 성능 문제가 확인된 부분에 대해 선택적으로 최적화를 적용하는 것이 원칙이다.
React 애플리케이션의 테스트는 컴포넌트의 격리된 동작 검증부터 여러 컴포넌트 간의 상호작용을 포괄적으로 검사하는 범위까지 이루어진다. 주로 Jest와 React Testing Library를 조합하여 사용하는 것이 일반적이다. Jest는 테스트 러너이자 단위 테스트 프레임워크 역할을 하며, React Testing Library는 컴포넌트를 사용자 관점에서 테스트할 수 있는 유틸리티를 제공한다[11].
단위 테스트는 함수, 훅, 개별 컴포넌트와 같은 단일 단위의 로직을 격리하여 검증하는 데 목적이 있다. 예를 들어, useState 훅을 사용하는 컴포넌트의 상태 변경이나 Props에 따른 조건부 렌더링을 테스트한다. 반면, 통합 테스트는 여러 컴포넌트가 함께 작동할 때의 흐름과 상호작용을 검증한다. 사용자의 클릭이나 입력을 시뮬레이션하여 폼 제출이나 페이지 네비게이션이 예상대로 수행되는지 확인하는 것이 대표적이다.
테스트 작성 시에는 구현 세부사항보다 컴포넌트의 실제 출력과 사용자 인터페이스를 검증하는 데 중점을 둔다. 다음은 일반적인 테스트 케이스의 예시이다.
테스트 유형 | 검증 대상 | 사용 도구 예시 |
|---|---|---|
렌더링 테스트 | 컴포넌트가 정상적으로 렌더링되고 특정 텍스트나 요소를 포함하는지 |
|
사용자 이벤트 테스트 | 버튼 클릭, 입력 변경 등 상호작용 후 UI가 올바르게 업데이트되는지 |
|
비동기 작업 테스트 | 데이터 fetching 후 상태 변경 및 렌더링 결과 |
|
커스텀 훅 테스트 | 훅의 반환값과 로직을 격리하여 테스트 |
|
효과적인 테스트 스위트를 구성하려면 비즈니스 로직이 집중된 핵심 컴포넌트와 사용자 흐름에 대한 테스트에 우선순위를 두어야 한다. 또한, Mocking을 통해 외부 API 호출이나 모듈을 가짜 구현으로 대체하여 테스트의 격리성과 속도를 보장한다.
React 애플리케이션의 단위 테스트는 개별 컴포넌트나 함수가 의도대로 동작하는지 격리하여 검증하는 과정이다. 주로 Jest와 React Testing Library를 조합하여 사용한다. Jest는 테스트 러너이자 어설션 라이브러리 역할을 하며, React Testing Library는 컴포넌트를 사용자 관점에서 렌더링하고 상호작용하는 API를 제공한다[12].
단위 테스트의 주요 대상은 프레젠테이션 컴포넌트의 렌더링 결과, Props를 통한 데이터 전달, 사용자 이벤트 처리의 정상 동작 등이다. 테스트는 컴포넌트가 특정 Props를 받았을 때 올바른 요소를 렌더링하는지, 버튼 클릭이나 입력 변경 시 핸들러 함수가 호출되는지 등을 검증한다. 커스텀 훅의 로직을 테스트할 때는 @testing-library/react-hooks 패키지를 활용하여 훅의 반환값과 상태 변화를 확인한다.
효과적인 단위 테스트를 작성하기 위한 몇 가지 원칙이 있다. 구현 세부사항보다 컴포넌트의 실제 사용자 인터페이스와 행위를 테스트해야 한다. data-testid 속성보다는 텍스트, 라벨, 역할(Role)을 통해 요소를 쿼리하는 것이 권장된다. 또한, 각 테스트는 하나의 사항만을 독립적으로 검증해야 하며, 테스트 간의 의존성을 피해야 한다.
테스트 대상 | 주요 검증 사항 | 사용 예시 도구/메서드 |
|---|---|---|
컴포넌트 렌더링 | 특정 요소나 텍스트가 존재하는지 |
|
Props 전달 | Props 값에 따라 렌더링이 달라지는지 |
|
사용자 이벤트 | 클릭, 입력 시 콜백이 실행되는지 |
|
비동기 작업 | 데이터 페칭 후 상태 업데이트 및 렌더링 |
|
커스텀 훅 | 상태 값과 함수의 동작 |
|
통합 테스트는 React 애플리케이션에서 여러 컴포넌트가 함께 작동하는 방식을 검증하는 테스트 수준이다. 단일 컴포넌트의 고립된 동작을 확인하는 단위 테스트와 달리, 컴포넌트 간의 상호작용, 상태 관리, 라우팅, API 호출 등의 실제 사용자 흐름을 시뮬레이션한다. 주로 사용자 시나리오(예: 로그인 후 목록 조회 및 항목 추가)를 중심으로 테스트 케이스를 구성하며, Jest와 React Testing Library가 널리 사용되는 도구 조합이다.
테스트는 실제 DOM 환경(예: jsdom)에서 렌더링된 컴포넌트 트리를 대상으로 진행된다. render, screen 객체를 사용하여 컴포넌트를 렌더링하고, userEvent 라이브러리로 클릭, 입력 등의 사용자 상호작용을 흉내 낸다. 그 후 expect와 screen.getBy... 쿼리 메서드를 조합해 예상되는 UI 변화나 상태를 단언한다. 외부 의존성(예: API)은 jest.mock 또는 MSW(Mock Service Worker) 같은 도구를 이용해 모킹하여 테스트의 안정성과 속도를 보장한다.
통합 테스트를 효과적으로 작성하기 위한 몇 가지 접근법은 다음과 같다.
접근법 | 설명 | 예시 |
|---|---|---|
사용자 흐름 중심 | 단순한 함수 호출 검증이 아닌, 사용자가 수행할 행동 순서대로 테스트를 구성한다. | "검색창에 텍스트 입력 → 제출 버튼 클릭 → 결과 목록이 표시됨"을 테스트. |
의존성 모킹 | 외부 API, Context, 라우터 등을 제어된 방식으로 모킹하여 테스트를 격리한다. |
|
쿼리 우선순위 준수 | React Testing Library의 권장 사항대로, 사용자가 접근하는 방식( |
|
비동기 작업 대기 | API 응답이나 상태 업데이트 후의 UI 변화를 테스트할 때 |
|
이러한 테스트는 애플리케이션의 핵심 기능 흐름이 정상적으로 작동함을 보장하며, 리팩토링 시의 회귀 버그를 방지하는 안전망 역할을 한다. 단위 테스트와 E2E 테스트 사이의 균형을 잡아, 높은 신뢰도를 유지하면서도 상대적으로 빠르게 실행되는 테스트 스위트를 구성하는 데 기여한다[13].