프로그래밍/ReactJS

React 컴포넌트: UI 개발의 핵심이자 생명주기 이해의 중요성

shimdh 2025. 9. 19. 10:50
728x90

React 개발의 세계에 깊이 발을 들여놓는다면, '컴포넌트'라는 개념을 빼놓고는 이야기할 수 없습니다. 컴포넌트는 모든 React 애플리케이션의 근간을 이루는 핵심 구성 요소이며, 사용자 인터페이스(UI)를 독립적이고 재사용 가능한 작은 단위로 분할하여 효율적으로 관리할 수 있도록 돕습니다. 오늘 이 글에서는 React 컴포넌트의 두 가지 주요 유형과 그들이 거치는 생명주기 단계에 대해 깊이 있게 탐구해보고자 합니다.


컴포넌트란 무엇이며 왜 중요한가?

React에서 컴포넌트는 UI의 특정 부분을 캡슐화하고 독립적으로 동작하게 만드는 블록입니다. 마치 레고 블록처럼, 개별 컴포넌트들을 조립하여 복잡한 UI를 구축할 수 있죠. 이는 코드의 재사용성을 높이고 유지보수를 용이하게 하며, 개발 과정을 훨씬 효율적으로 만듭니다.


728x90

React 컴포넌트의 두 가지 유형

React는 개발자가 필요에 따라 선택할 수 있는 두 가지 주요 컴포넌트 유형을 제공합니다.

  1. 함수 컴포넌트 (Function Components)
    • JSX(JavaScript XML)를 반환하는 단순한 JavaScript 함수입니다.
    • 주로 UI를 표시하는 '무상태(stateless)' 컴포넌트에 사용되었으나, React Hooks의 등장으로 이제는 상태 관리 및 생명주기 기능도 활용할 수 있게 되었습니다.
    • 간결하고 직관적인 코딩이 가능하여 최근 React 개발의 주류를 이루고 있습니다.
function Greeting(props) {
    return <h1>안녕하세요, {props.name}님!</h1>;
}
  1. 클래스 컴포넌트 (Class Components)
    • React.Component를 상속받아 확장하는 ES6 클래스입니다.
    • 지역 상태 관리(local state management) 및 생명주기 메서드(lifecycle methods)와 같은 더 많은 기능을 제공합니다.
    • 복잡한 로직이나 상태 변화가 필요한 경우에 유용하게 사용되었지만, 함수 컴포넌트와 Hooks의 발전으로 인해 사용이 점차 줄어들고 있는 추세입니다.

class Greeting extends React.Component {
    render() {
        return <h1>안녕하세요, {this.props.name}님!</h1>;
    }
}

두 유형 모두 UI를 구성한다는 동일한 목적을 가지지만, 문법과 제공하는 기능 면에서 차이가 있습니다. 프로젝트의 복잡성, 팀의 선호도, 그리고 필요한 기능에 따라 적절한 컴포넌트 유형을 선택하는 것이 중요합니다.


React 컴포넌트 생명주기: 탄생부터 소멸까지

모든 React 컴포넌트는 애플리케이션 내에서 생성되고, 업데이트되며, 최종적으로 제거되는 일련의 단계를 거칩니다. 이를 '컴포넌트 생명주기(Component Lifecycle)'라고 부르며, 각 단계마다 특정 시점에 자동으로 호출되는 '생명주기 메서드'가 존재합니다. 개발자는 이 메서드들을 오버라이드하여 컴포넌트의 특정 시점에서 원하는 코드를 실행할 수 있습니다. 생명주기를 깊이 이해하는 것은 복잡한 상태 변화를 관리하고, 외부 API와 연동하며, 애플리케이션 성능을 최적화하는 데 필수적입니다.

1. 마운팅 (Mounting) 단계: 컴포넌트의 탄생

이 단계는 컴포넌트가 처음 생성되어 DOM(문서 객체 모델)에 삽입될 때 발생합니다. 즉, 화면에 컴포넌트가 처음 나타나는 시점입니다.

  • constructor():
    • 컴포넌트가 생성될 때 가장 먼저 호출되는 메서드입니다.
    • 컴포넌트의 초기 상태(this.state)를 설정하거나, 이벤트 핸들러를 바인딩하는 데 주로 사용됩니다.
    • super(props)를 호출하여 부모 클래스의 생성자를 호출해야 합니다.
  • static getDerivedStateFromProps() (드물게 사용):
    • 렌더링 전에 props로부터 파생된 상태를 업데이트할 때 사용됩니다.
    • 자주 사용되지는 않으며, 상태를 동기화하는 데 명확한 이유가 있을 때만 고려해야 합니다.
  • render():
    • 컴포넌트가 렌더링될 내용을 정의하는 유일한 필수 메서드입니다.
    • JSX를 반환해야 하며, 이 메서드 내에서는 상태를 직접 변경해서는 안 됩니다. (순수 함수여야 합니다)
  • componentDidMount():
    • 컴포넌트가 DOM에 마운트된 직후 호출됩니다.
    • 데이터 가져오기(fetching data), 구독(subscriptions) 설정, DOM 노드 직접 조작과 같은 초기화 작업에 이상적인 시점입니다.

2. 업데이트 (Updating) 단계: 변화에 반응하다

이 단계는 컴포넌트의 props 또는 state에 변경 사항이 발생할 때마다 발생합니다. React는 변경을 감지하고, 필요한 경우 UI를 다시 렌더링합니다.

  • static getDerivedStateFromProps() (다시 호출):
    • 새로운 props나 state에 따라 상태를 업데이트해야 할 때 호출됩니다.
  • shouldComponentUpdate(nextProps, nextState):
    • props 또는 state 변경에 따라 리렌더링이 필요한지 여부를 결정합니다.
    • true를 반환하면 컴포넌트가 업데이트되고, false를 반환하면 업데이트가 건너뛰어집니다.
    • 불필요한 렌더링을 방지하여 성능을 최적화하는 데 기여할 수 있지만, 신중하게 사용해야 합니다.
  • render() (다시 호출):
    • 새로운 propsstate를 기반으로 UI를 다시 렌더링합니다.
  • getSnapshotBeforeUpdate(prevProps, prevState) (드물게 사용):
    • 렌더링 결과가 DOM에 반영되기 직전에 호출됩니다.
    • DOM에서 스크롤 위치와 같은 정보를 가져와 componentDidUpdate로 전달하는 데 유용합니다.
  • componentDidUpdate(prevProps, prevState, snapshot):
    • 업데이트가 발생한 직후 호출됩니다.
    • 이전 propsstate를 현재 propsstate와 비교하여 특정 조건에 따라 작업을 수행하는 데 유용합니다 (예: 네트워크 요청).

3. 언마운팅 (Unmounting) 단계: 깔끔한 마무리

이 단계는 컴포넌트가 DOM에서 제거될 때 발생합니다. 메모리 누수를 방지하고 리소스를 해제하는 중요한 시점입니다.

  • componentWillUnmount():
    • 컴포넌트가 DOM에서 제거되기 직전에 호출됩니다.
    • 타이머 무효화(clearInterval), 네트워크 요청 취소, 구독 해제와 같은 정리 작업(cleanup tasks)에 유용합니다.
    • 이 메서드에서 리소스를 해제하는 것이 메모리 누수를 방지하는 데 매우 중요합니다.

실용적인 예시: 카운터 애플리케이션으로 생명주기 이해하기

클래스 컴포넌트와 생명주기 메서드를 사용하여 간단한 카운터 애플리케이션을 만들어보며 위 개념들을 더 깊이 이해해봅시다.

class Counter extends React.Component {
    constructor(props) {
        super(props);
        this.state = { count: 0 };
        this.increment = this.increment.bind(this);
    }

    // 마운팅 단계: 컴포넌트가 DOM에 추가된 직후
    componentDidMount() {
        console.log('카운터가 마운트되었습니다');
        // 예시: 1초마다 count를 증가시키는 인터벌 설정
        this.interval = setInterval(() => {
            console.log('인터벌이 실행 중입니다');
            this.setState(prevState => ({ count: prevState.count + 1 }));
        }, 1000);
    }

    // 업데이트 단계: props 또는 state 변경 시 리렌더링 여부 결정
    shouldComponentUpdate(nextProps, nextState) {
        // count가 5의 배수일 때만 업데이트
        console.log('shouldComponentUpdate가 호출되었습니다. nextState.count:', nextState.count);
        return nextState.count % 5 === 0 || nextState.count < 5;
    }

    // 업데이트 단계: 업데이트가 발생한 직후
    componentDidUpdate(prevProps, prevState) {
        console.log('카운터가 업데이트되었습니다. 이전 값:', prevState.count, '현재 값:', this.state.count);
    }

    // 언마운팅 단계: 컴포넌트가 DOM에서 제거되기 직전
    componentWillUnmount() {
        console.log('카운터가 언마운트됩니다. 인터벌을 정리합니다.');
        clearInterval(this.interval);
    }

    increment() {
        this.setState(prevState => ({ count: prevState.count + 1 }));
    }

    render() {
        console.log('render()가 호출되었습니다.');
        return (
            <div>
                <h2>카운터: {this.state.count}</h2>
                <button onClick={this.increment}>증가</button>
            </div>
        );
    }
}

이 코드를 실행하고 콘솔을 확인해보면, 컴포넌트가 생성, 업데이트, 제거되는 과정에서 각 생명주기 메서드가 언제 호출되는지 명확하게 이해할 수 있습니다. 특히 shouldComponentUpdate를 통해 불필요한 렌더링을 막아 성능을 최적화하는 방법을 볼 수 있습니다.


결론: 왜 생명주기 이해가 중요한가?

React 개발에서 컴포넌트 생명주기를 이해하는 것은 단순히 지식을 쌓는 것을 넘어섭니다. 이는 곧 애플리케이션의 동작 방식과 성능을 완벽하게 제어할 수 있는 능력을 의미합니다. 올바른 시점에 데이터를 가져오고, 불필요한 리소스를 해제하며, 사용자 경험을 최적화하는 것 모두 생명주기에 대한 깊은 이해에서 비롯됩니다.

함수 컴포넌트와 Hooks가 대세가 되면서 클래스 컴포넌트의 생명주기 메서드들이 useEffect와 같은 Hook으로 대체되고 있지만, 그 근본적인 개념과 원리는 여전히 동일합니다. 컴포넌트의 탄생과 소멸을 이해하는 것은 어떤 기술 스택을 사용하든 견고한 React 애플리케이션을 구축하는 데 필수적인 기초가 됩니다.

728x90