- Published on
2025.03.21
[리액트 딥다이브] - 02. 리액트 핵심 요소 깊게 살펴보기(2)
2.3. 클래스 컴포넌트와 함수 컴포넌트
2.3.1. 클래스 컴포넌트
리액트 16.8 이전엔 클래스 컴포넌트가 대다수.
import React from 'react'
// props 타입 선언
interface SampleProps {
required?: boolean
text: string
}
// state 타입 선언
interface SampleState {
count: number
isLimited?: boolean
}
// Component에 제네릭으로 props, state를 순서대로 넣어준다.
class SampleComponent extends React.Component<SampleProps, SampleState> {
// constructor에서 props를 넘겨주고, state 기본값을 설정
private constructor(props: SampleProps) {
super(props)
this.state = {
count: 0,
isLimited: false,
}
}
// render 내부에서 쓰일 함수 선언
private handleClick = () => {
const newValue = this.state.count + 1
this.setState({ count: newValue, isLimited: newValue >= 10 })
}
// render에서 컴포넌트가 렌더링할 내용 정의
public render() {
// props와 state 값을 this, 즉 해당 클래스에서 꺼냄
const {
props: { required, text },
state: { count, isLimited },
} = this
return (
<h2>
Sample Component
<div>{required ? '필수' : '필수아님'}</div>
<div>문자: {text}</div>
<div>count: {count}</div>
<button onClick={this.handleClick} disabled={isLimited}>
증가
</button>
</h2>
)
}
}
constructor()
: 컴포넌트가 초기화되는 시점에 호출되어 state를 초기화. ES2022에 추가된 클래스 필드로 인해 별도의 초기화 과정을 거치지 않고 클래스 내부에 필드를 선언할 수 있게 됨props
: 컴포넌트에 특정 속성을 전달state
: 클래스 컴포넌트 내부에서 관리하는 값이며 항상 객체여야 함, 변화가 있을 때마다 리렌더링 발생메서드
: 렌더링 함수 내부에서 사용되는 함수, 보통 DOM에서 발생하는 이벤트와 함께 사용
클래스 컴포넌트의 생명주기 메서드
클래스 컴포넌트는 생명주기 메서드에 의존, 생명주기 메서드가 실행되는 시점은 3가지
mount
: 컴포넌트가 마운팅(생성)되는 시점update
: 이미 생성된 컴포넌트의 내용이 변경(업데이트)되는 시점unmount
: 컴포넌트가 더 이상 존재하지 않는 시점
생명주기 메서드
render()
리액트 클래스 컴포넌트의 유일한 필수 값, 컴포넌트가 UI를 렌더링하기 위해 사용, 마운트와 업데이트 과정에서 일어남
순수 함수여야 하므로 render() 내부에서 this.setState룰 호출하면 안 됨componentDidMount()
클래스 컴포넌트가 마운트되고 준비되면 호출, this.setState를 호출하고 state가 변경되면 즉시 리렌더링을 시도하며, 브라우저가 UI를 업데이트하기 전에 실행되어 사용자는 변경을 눈치챌 수 없음
성능 문제를 일으킬 수 있으므로 state를 다루는 것은 생성자에서 하는 것이 좋음componentDidUpdate()
컴포넌트 업데이트 후 바로 실행, state나 props의 변화에 따라 DOM을 업데이트componentWillUnmount()
컴포넌트가 언마운트되거나 더 이상 사용되지 않기 직전에 호출, 클린업 함수를 호출하기 위한 최적의 위치shouldComponentUpdate()
state나 props의 변경으로 리액트 컴포넌트가 리렌더링되는 것을 막고 싶을 때 사용, 컴포넌트에 영향을 받지 않는 변화에 대해 정의static getDerivedStateFromProps()
사라진 componentWillReceiveProps를 대체하는 메서드, render()를 호출하기 직전에 호출
static으로 선언되어 있어 this에 접근할 수 없음getSnapShotBeforeUpdate()
componentWillUpdate()를 대체하는 메서드, DOM이 업데이트되기 직전에 호출
DOM에 렌더링되기 전 윈도우 크기 조절이나 스크롤 위치 조정 등의 작업 처리에 유용getDerivedStateFromError()
에러 상황에서 실행되는 메서드, 자식 컴포넌트에서 에러가 발생했을 때 호출되는 에러 메서드componentDidCatch
에러 상황에서 실행되는 메서드, 자식 컴포넌트에서 에러가 발생했을 때 실행되며, getDerivedStateFromError에서 에러를 잡고 state를 결정한 이후 실행
getDerivedStateFromError()에서 하지 못한 부수 효과를 수행할 수 있음, render 단계가 아닌 커밋 단계에서 실행되기 때문getSnapShotBeforeUpdate(), getDerivedStateFromError(), componentDidCatch까지 세 메서드는 리액트 훅으로 구현되어 있지 않아 반드시 클래스 컴포넌트를 사용해야 함.
클래스 컴포넌트의 한계
- 데이터 흐름 추적이 어려움
- 애플리케이션 내부 로직 재사용이 어려움
- 기능이 많아질수록 컴포넌트의 크기가 커짐
- 클래스는 함수보다 상대적으로 어려움
- 코드 크기 최적화가 어려움
- 핫 리로딩에 상대적으로 불리
2.3.2. 함수 컴포넌트
16.8에서 함수 컴포넌트에서 사용 가능한 훅이 등장
2.3.3. 함수 컴포넌트 vs. 클래스 컴포넌트
생명주기 메서드의 부재
함수 컴포넌트는 props를 받아 단순히 리액트 요소만 반환하는 함수,
클래스 컴포넌트는 render 메서드가 있는 React.component를 상속받아 구현하는 자바스크립트 클래스useEffect
훅으로 생명주기 메서드(componentDidMount, componentDidUpdate, componentWillUnmount)를 비슷하게 구현할 수 있음useEffect
는 생명주기를 위한 훅이 아님!
함수 컴포넌트와 렌더링된 값
함수 컴포넌트는 렌더링된 값을 고정하고, 클래스 컴포넌트는 그렇지 못함 클래스 컴포넌트는 props의 값을 항상 this로부터 가져오는데, this가 가리키는 객체, 즉 컴포넌트의 인스턴스의 멤버는 변경 가능한 값이기 때문. 함수 컴포넌트는 렌더링이 일어날 때마다 그 순간의 값인 props와 state를 기준으로 렌더링됨 (state가 상수인 이유라고 생각)
2.4. 렌더링은 어떻게 일어나는가?
2.4.1. 리액트의 렌더링이란?
리액트의 렌더링을 브라우저의 렌더링과 혼동해서 사용하면 안 됨
리액트의 렌더링은 리액트 애플리케이션 트리 안에 있는 모든 컴포넌트들이 현재 자신들이 가지고 있는 props와 state의 값을 기반으로 어떻게 UI를 구성하고 DOM 결과를 브라우저에 제공할지 계산하는 과정을 의미
2.4.2. 리액트의 렌더링이 일어나는 이유
- 최초 렌더링: 처음 애플리케이션 진입 시 수행
- 리렌더링: 최초 렌더링 발생 후 일어나는 모든 렌더링, 리렌더링이 발생하는 경우, 이외의 경우는 렌더링을 일으키지 않는다.
- 클래스 컴포넌트의 setState가 실행
- 클래스 컴포넌트의 forceUpdate가 실행
- 함수 컴포넌트의
useState()
의 두 번째 배열 요소인 setter가 실행 - 함수 컴포넌트의
useReducer()
의 두 번째 배열 요소인 dispatch가 실행 - 컴포넌트의 key props가 변경
- props가 변경
- 부모 컴포넌트가 렌더링: 부모 컴포넌트가 리렌더링되면 자식 컴포넌트도 무조건 리렌더링 된다.
2.4.3. 리액트의 렌더링 프로세스
렌더링 프로세스가 시작되면 리액트는 컴포넌트의 루트부터 아래쪽으로 가며 업데이트가 필요하다고 지정된 모든 컴포넌트를 찾음
업데이트가 필요한 컴포넌트를 발견하면 클래스 컴포넌트는 render() 함수를 실행, 함수 컴포넌트는 FunctionComponent() 그 자체를 호출한 뒤, 결과물을 저장함
가상 DOM과 비교해 실제 DOM에 반영하기 위한 모든 변경 사항을 수집함(재조정)
재조정 과정이 끝나면 변경 사항을 하나의 동기 시퀀스로 DOM에 적용
리액트의 렌더링은 렌더 단계
와 커밋 단계
로 분리되어 실행된다.
2.4.4. 렌더와 커밋
렌더 단계
컴포넌트를 렌더링하고 변경 사항을 계산하는 모든 작업
렌더링 프로세스에서 컴포넌트를 실행한 결과와 이전 가상 DOM을 비교하는 과정을 거쳐 변경이 필요한 컴포넌트를 체크하는 단계type
,props
,key
세 가지를 비교해 하나라도 변경된 것이 있으면 체크해 둔다.커밋 단계
렌더 단계의 변경 사항을 실제 DOM에 적용해 사용자에게 보여주는 과정
커밋 단계가 끝나야 브라우저의 렌더링이 발생
리액트의 렌더링이 일어난다고 해서 무조건 DOM 업데이트가 일어나는 것은 아니다. 리액트의 렌더링을 수행했으나 변경 사항이 감지되지 않는다면 커밋 단계는 생략될 수 있기 때문.
2.5. 컴포넌트와 함수의 무거운 연산을 기억해 두는 메모이제이션
메모이제이션 기법은 언제 사용하는 것이 좋을까?
useMemo
, useCallback
훅과 고차 컴포넌트 memo
는 리액트의 렌더링을 최소한으로 줄이기 위해 제공된다.
메모이제이션 최적화와 관련한 논쟁들이 지속되고 있다. 아래 내용은 메모이제이션에 대한 주장들
2.5.1. 주장 1: 섣부른 최적화는 독이다. 꼭 필요한 곳에만 메모이제이션을 추가하자
가벼운 작업이라면 메모이제이션해서 자바스크립트 메모리에 두고 꺼내는 것보다 매번 작업을 수행하는 것이 빠를 수 있음
그러나 메모이제이션도 비용이 든다.
- 값을 비교하고 렌더링 또는 재계산이 필요한지 확인하는 작업
- 이전 결과물을 저장해두고 다시 꺼내오는 작업
메모이제이션은 신중하게 접근해야 하고 섣부른 최적화(premature optimization)를 항상 경계해야 한다. 메모이제이션은 트레이드 오프가 있는 기법으로, 메모이제이션으로 인한 성능 개선이 렌더링보다 낫지 않다면 사용하지 않는 것이 낫다.
개발자가 미리 렌더링이 많이 될 것 같은 부분을 예측해 메모이제이션하는 섣부른 최적화는 옳지 못한 행동이다. 일단 애플리케이션을 만든 후, 개발자 도구나 useEffect
를 사용해 렌더링을 확인하고 필요한 곳에서만 최적화하는 것이 옳다.
2.5.2. 주장 2: 렌더링 과정의 비용은 비싸다. 모조리 메모이제이션해 버리자
최적화나 성능 향상에 쏟을 시간은 많지 않다. 리액트는 재조정 알고리즘을 사용하므로 다음 렌더링과 구별하기 위해 이전 렌더링 결과를 저장하고 있다. 따라서 memo로 지불해야 하는 비용은 props에 대한 얕은 비교뿐으로, memo를 하지 않았을 때의 비용이 다음과 같이 더 크다.
- 렌더링을 함으로써 발생하는 비용
- 컴포넌트 내부 복잡한 로직의 재실행
- 위 두 가지가 자식 컴포넌트에서 반복해서 발생
- 리액트가 이전 트리와 새 트리를 비교
메모이제이션을 했을 때, 하지 않는 것보다 많은 이점이 있다. 섣부른 초기화라고 해도 최적화에 대한 확신이 없다면 가능한 모든 곳에 메모이제이션 활용해 최적화를 하는 것이 좋다.