Programmers

[45일차]리액트(React) 컴포넌트와 상태 관리.

PARKpatchnotes 2025. 11. 13. 16:11

1. 클래스형 컴포넌트 (Class Component)

클래스형 컴포넌트는 ES6의 class 문법을 사용하여 정의하는 컴포넌트이다. 리액트의 React.Component를 상속받아 구현하며, 과거 리액트에서 상태 관리와 생명주기(Lifecycle) 기능을 사용하기 위한 표준적인 방법이었다.

  • 특징:

    • constructor에서 this.state를 통해 컴포넌트의 상태(State)를 초기화한다.
    • componentDidMount, componentDidUpdate, componentWillUnmount와 같은 생명주기 메서드를 사용하여 특정 시점에 코드를 실행할 수 있다.
    • render() 메서드에서 렌더링할 JSX를 반환한다.
    • 컴포넌트의 상태와 메서드에 접근할 때 this 키워드를 사용해야 한다.
  • 예시 코드:

    import React, { Component } from 'react'; 
    
    class ClassCounter extends Component { 
      constructor(props) { 
        super(props); 
        this.state = { 
          count: 0, 
        }; 
      } 
    
      handleIncrement = () => { 
        this.setState({ 
          count: this.state.count + 1 
        }); 
      }; 
    
      render() { 
        return ( 
          <div> 
            <p>Count: {this.state.count}</p> 
            <button onClick={this.handleIncrement}>Increment</button> 
          </div> 
        ); 
      } 
    }

리액트 훅(Hook)이 도입된 이후로는 복잡한 this 바인딩과 상대적으로 긴 코드 구조 때문에 특별한 경우가 아니면 함수형 컴포넌트를 사용하는 것이 권장된다.


2. 함수형 컴포넌트 (Functional Component)

함수형 컴포넌트는 자바스크립트 함수를 사용하여 정의하는 컴포넌트이다. 초기에는 상태를 가질 수 없는 단순한 UI 렌더링 용도로 사용되었으나, 리액트 훅(React Hooks)이 도입되면서 상태 관리와 생명주기 기능을 모두 사용할 수 있게 되었다.

  • 특징:

    • 코드가 클래스형 컴포넌트에 비해 더 간결하고 직관적이다.
    • useState, useEffect 등의 훅을 통해 상태와 생명주기 관련 로직을 구현한다.
    • this 키워드를 사용하지 않아 복잡성이 줄어든다.
    • 현대 리액트 개발의 표준 방식으로 자리 잡았다.
  • 예시 코드:

    import React, { useState } from 'react'; 
    
    function FunctionalCounter() { 
      const [count, setCount] = useState(0); 
    
      const handleIncrement = () => { 
        setCount(count + 1); 
      }; 
    
      return ( 
        <div> 
          <p>Count: {count}</p> 
          <button onClick={handleIncrement}>Increment</button> 
        </div> 
      ); 
    }

3. 변수와 State의 차이

리액트 컴포넌트 내에서 데이터를 다룰 때 일반 변수와 state는 명확한 차이를 가진다.

  • 일반 변수 (Variable):

    • 컴포넌트가 다시 렌더링될 때마다 초기화된다. 값이 유지되지 않는다.
    • 변수의 값이 변경되더라도 리액트는 이를 감지하지 못하며, 화면을 다시 렌더링하지 않는다(No Re-render). 따라서 UI가 업데이트되지 않는다.
  • 상태 (State):

    • 리액트가 직접 관리하는 데이터로, 컴포넌트의 생명주기 동안 값이 유지된다.
    • setState와 같은 상태 변경 함수를 통해 값이 업데이트되면, 리액트는 이 변화를 감지하고 컴포넌트를 다시 렌더링한다(Trigger Re-render). 이로 인해 변경된 state 값이 UI에 반영된다.

결론적으로, UI에 동적으로 반영되어야 하는 값은 반드시 state로 관리해야 한다.


4. useState

useState는 함수형 컴포넌트 내에서 상태를 추가하고 관리할 수 있게 해주는 가장 기본적인 훅(Hook)이다.

  • 문법:
    const [state이름, state변경함수] = useState<타입>(초기값);
    useState는 배열을 반환하며, 이 배열은 두 개의 요소를 포함한다.
    1. state이름: 현재 상태 값.
    2. state변경함수: 상태를 업데이트하는 함수. 관례적으로 상태 이름 앞에 set을 붙여 작명한다 (예: count -> setCount).

5. 상태 변경과 렌더링

함수형 업데이트(prev)를 사용해야 하는 이유

리액트의 상태 업데이트는 비동기적으로 처리될 수 있으며, 성능 최적화를 위해 여러 개의 상태 업데이트를 하나로 묶어 처리하는 배치(Batching) 작업을 수행한다.

  • 문제점:
    짧은 시간 안에 여러 번의 상태 업데이트가 발생하는 경우, 이전 상태 값을 기반으로 다음 상태를 계산하면 오래된 상태(Stale State) 값을 참조할 위험이 있다.

    // 잘못된 예: 1씩 3번 증가시키려 했지만, 실제로는 1만 증가할 수 있다.
    setCount(count + 1); 
    setCount(count + 1); 
    setCount(count + 1);
  • 해결책: 함수형 업데이트
    상태 변경 함수에 값을 직접 전달하는 대신, 콜백 함수를 전달할 수 있다. 이 콜백 함수는 이전 상태 값(prev)을 인자로 받아 새로운 상태 값을 반환한다. 리액트는 이 콜백 함수가 실행될 때, 가장 최신의 상태 값을 prev 인자로 전달하는 것을 보장한다. 따라서 상태가 이전 값에 의존하는 경우, 항상 함수형 업데이트를 사용하는 것이 안전하고 예측 가능하다.

    // 올바른 예: 이전 상태를 기반으로 안전하게 업데이트한다.
    setCount(prevCount => prevCount + 1); 
    setCount(prevCount => prevCount + 1); 
    setCount(prevCount => prevCount + 1);

6. 반복문을 위한 map 함수

리액트에서 배열의 각 요소를 UI 컴포넌트로 변환하여 렌더링할 때 map 함수를 사용한다.

  • el: 배열의 각 요소를 의미한다.
  • index: 배열의 각 요소의 인덱스를 의미한다.
  • key: map을 사용하여 리스트를 렌더링할 때, 각 요소는 고유한 key prop을 가져야 한다. key는 리액트가 어떤 항목이 변경, 추가, 또는 삭제되었는지 식별하는 것을 돕는다. key 값으로는 보통 각 요소의 고유 ID를 사용하며, 고유 ID가 없을 경우 최후의 수단으로 index를 사용할 수 있지만 권장되지 않는다.

forEach와의 차이점

  • forEach: 반환 값이 없는(undefined) 메서드이다. 배열의 각 요소를 순회하며 주어진 작업을 수행할 뿐, 새로운 배열을 생성하지 않는다. JSX는 undefined를 렌더링하지 않으므로, forEach는 UI 렌더링에 사용할 수 없다.

  • map: 배열의 각 요소에 대해 주어진 함수를 실행한 결과를 모아 새로운 배열을 반환한다. 이 반환된 JSX 요소 배열을 리액트가 렌더링할 수 있다.

  • 예시 코드:

    function FruitList() { 
      const fruits = [ 
        { id: 1, name: 'Apple' }, 
        { id: 2, name: 'Banana' }, 
        { id: 3, name: 'Cherry' }, 
      ]; 
    
      return ( 
        <ul> 
          {/* fruits 배열을 순회하며 각 요소를 <li> 태그로 변환 */}
          {fruits.map(fruit => ( 
            <li key={fruit.id}>{fruit.name}</li> 
          ))} 
        </ul> 
      ); 
    }