코딩/리액트

[새싹 성동 2기] 리액트 상태(state) 에 대하여

insu90 2024. 11. 19. 00:37

리액트(React)는 상태(state)를 통해 컴포넌트의 동적인 동작을 가능하게 합니다. 상태는 일반 변수와 달리 변경 시 컴포넌트를 리렌더링하여 사용자 인터페이스(UI)에 즉각적으로 반영됩니다. 이 글에서는 상태의 정의, 클래스형 및 함수형 컴포넌트에서 상태를 관리하는 방법, 그리고 상태 관리 시 주의사항을 살펴보겠습니다.

 

1. 상태(state)란?

상태는 컴포넌트 내부에서 읽고 업데이트할 수 있는 데이터입니다. 리액트는 상태가 변경될 때 컴포넌트를 자동으로 리렌더링합니다.

중요: 상태 변수는 일반 변수처럼 직접 수정할 수 없으며, 반드시 리액트가 제공하는 setState 메서드 또는 useState 훅을 사용해 업데이트해야 합니다.

클래스형은 setState, 함수형은 useState가 반환하는 업데이트 함수를 이용해서 변경해야 합니다.

 

2. 클래스형 컴포넌트에서의 상태 관리

상태 정의 방법

클래스형 컴포넌트에서는 state 키워드를 사용하거나 생성자(constructor)에서 상태를 초기화할 수 있습니다.

src 디렉터리 아래에 state 디렉터리를 만들고, Counter.js 파일을 생성

import { Component } from "react";

class Counter extends Component {
    // 클래스형 컴포넌트에서 상태변수를 정의하는 방법
    // 방법1. 생성자 함수 내에 state 변수의 값으로 추가
    /*
    constructor(props) {
        super(props);
        this.state = {
            number: 0
        };
    }
    */

    // 방법2. state 키워드를 이용해서 상태변수를 정의 
    state = {
        number: 0
    };

    render() {
        return (
            <>
                {/* 상태 변수 참조(사용) */}
                <h1>{this.state.number}</h1>

                {/* 상태 변수의 값을 변경할 때는 setState() 메서드를 이용 */}
                <button onClick={() => {
                    this.setState({ number: this.state.number+1 })
                }}>하나 증가</button>
            </>
        );
    }
}

export default Counter;

App.js 파일에 Counter 컴포넌트를 추가

import Counter from "./state/Counter";

function App() {
  return (
    <>
      <Counter />
    </>
  );
}
export default App;

 

상태 변수 여러 개 관리

state 객체에 여러 상태 변수를 정의하면 각각 독립적으로 관리할 수 있습니다.

상태 변수가 여러 개인 경우 ⇒ 상태 변수에 fixedNumber를 추가하고 초기값으로 10을 할당해보겠습니다.

import { Component } from "react";

class Counter extends Component {
    state = {
        number: 0,
        fixedNumber: 10
    };

    render() {
        // 객체 비구조화를 통해서 상태변수를 지역변수로 할당
        const { number, fixedNumber } = this.state;
        return (
            <>
                <h1>{number}</h1>

                <button onClick={() => {
                    this.setState({ number: number + 1 })
                }}>하나 증가</button>

                <h1>{fixedNumber}</h1>
            </>
        );
    }
}

export default Counter;

실행하면  setState 메서드를 통해서 전달한 상태변수 값만 변경되는 것을 확인할 수 있습니다.

상태를 참조할 때는 객체 비구조화 문법을 활용해 깔끔하게 작성할 수 있습니다.

 

연속적인 setState 호출 문제

여러 번 setState를 호출할 경우, 상태 값이 즉각 반영되지 않을 수 있습니다.

setState() 메서드를 연속해서 호출하는 경우 ⇒ 다섯 증가 버튼을 추가 ⇒ 1씩 증가하는 것을 확인할 수 있습니다.

import { Component } from "react";

class Counter extends Component {
    state = {
        number: 0,
        fixedNumber: 10
    };

    render() {
        // 객체 비구조화를 통해서 상태변수를 지역변수로 할당
        const { number, fixedNumber } = this.state;
        return (
            <>
                <h1>{number}</h1>

                <button onClick={() => {
                    this.setState({ number: number + 1 });
                }}>하나 증가</button>
                <button onClick={() => {
                    this.setState({ number: number + 1 });	⇐ 객체를 전달하는 방식
                    this.setState({ number: number + 1 });
                    this.setState({ number: number + 1 });
                    this.setState({ number: number + 1 });
                    this.setState({ number: number + 1 });
                }}>다섯 증가</button>

                <h1>{fixedNumber}</h1>
            </>
        );
    }
}

export default Counter;

이는 객체를 전달하는 방식이므로 의도와 다르게 하나씩만 증가하는 것을 알 수 있습니다.

 

setState() 메서드에 updater 함수를 이용 ⇒ 다섯 증가 버튼을 클릭하면 5씩 증가되는 것을 확인하겠습니다.

import { Component } from "react";

class Counter extends Component {
    state = {
        number: 0,
        fixedNumber: 10
    };

    render() {
        // 객체 비구조화를 통해서 상태변수를 지역변수로 할당
        const { number, fixedNumber } = this.state;
        return (
            <>
                <h1>{number}</h1>

                <button onClick={() => {
                    this.setState({ number: number + 1 });
                }}>하나 증가</button>
                <button onClick={() => {
                    this.setState(prevState => ({ number: prevState.number + 1 }));	⇐ 함수를 전달하는 방식
                    this.setState(prevState => ({ number: prevState.number + 1 }));
                    this.setState(prevState => ({ number: prevState.number + 1 }));
                    this.setState(prevState => ({ number: prevState.number + 1 }));
                    this.setState(prevState => ({ number: prevState.number + 1 }));
                }}>다섯 증가</button>

                <h1>{fixedNumber}</h1>
            </>
        );
    }
}

export default Counter;
prevState는 값이 누적되어 의도대로 5가 증가됨을 볼 수 있습니다.

상태 변수 업데이트 후 특정 작업을 수행 ⇒ setState 메서드에 두번째 매개변수에 콜백 함수를 등록하면, 상태 변수가 바뀌면 변경된 값을 해당 콜백 함수에서 사용 ⇒ 상태 변수가 바뀌면 변경된 상태 변수를 로그로 기록하도록 수정해보겠습니다.

import { Component } from "react";

class Counter extends Component {
    // 컴포넌트 상태(state)를 정의
    state = {
        number: 0,       // 현재 카운터 값
        fixedNumber: 10  // 고정된 숫자, 변하지 않음
    };

    render() {
        // 객체 비구조화를 통해 this.state의 값을 지역변수로 할당
        const { number, fixedNumber } = this.state;

        return (
            <>
                {/* 현재 상태값(number)을 화면에 출력 */}
                <h1>{number}</h1>

                {/* 버튼 클릭 시 number 값을 1 증가 */}
                <button onClick={() => {
                    // 상태 변경 및 상태 변경 후의 number 값을 출력
                    this.setState(
                        { number: number + 1 }, // 상태 업데이트
                        () => console.log(`변경 후: ${this.state.number}`) // 업데이트 이후 실행될 콜백 함수
                    );
                }}>하나 증가</button>

                {/* 버튼 클릭 시 number 값을 5 증가 */}
                <button onClick={() => {
                    // 첫 번째 setState: 이전 상태(prevState)의 값을 기반으로 업데이트
                    this.setState(
                        prevState => {
                            console.log(`변경 전 (prevState.number): ${prevState.number}`); // 이전 상태 출력
                            console.log(`변경 전 (this.state.number): ${this.state.number}`); // 현재 상태 출력
                            return { number: prevState.number + 1 }; // number를 1 증가
                        },
                        () => console.log(`변경 후: ${this.state.number}`) // 업데이트 이후 실행될 콜백 함수
                    );

                    // 연속적으로 setState 호출
                    // React에서는 setState가 비동기로 처리되므로, 이 호출들은 일괄적으로 처리됨
                    this.setState(prevState => ({ number: prevState.number + 1 })); // 2 증가
                    this.setState(prevState => ({ number: prevState.number + 1 })); // 3 증가
                    this.setState(prevState => ({ number: prevState.number + 1 })); // 4 증가
                    this.setState(prevState => ({ number: prevState.number + 1 })); // 5 증가
                }}>다섯 증가</button>

                {/* 고정된 숫자(fixedNumber)를 화면에 출력 */}
                <h1>{fixedNumber}</h1>
            </>
        );
    }
}

export default Counter;
 
 
 
 
3. 함수형 컴포넌트에서의 상태 관리

함수형 컴포넌트에서는 리액트의 useState 훅을 사용해 상태를 정의합니다.

src\state 디렉터리 아래에 Say.js 파일을 추가하고 함수형 컴포넌트를 구현 ⇒ 입장, 퇴장 버튼을 제공하고 각 버튼을 클릭하면 메시지를 출력해 보겠습니다.

import { useState } from "react";

function Say() {
    // 함수형 컴포넌트에서는 상태변수를 정의할 때 useState 훅을 이용 
    const [message, setMessage] = useState('');

    // 특정 이벤트가 발생했을 때 동작을 함수로 정의 = 이벤트 핸들러
    // 일반적으로 do 또는 handle(handler)와 같은 접두어를 사용해서 정의 
    const doEnterClick = () => setMessage('입장합니다.');
    const doLeaveClick = () => setMessage('퇴장합니다.');
    //                         ~~~~~~~~~~~~~~~~~~~~~~~~~~
    //                         상태변수의 값을 변경할 때는 useState 훅 함수가 반환한 세터 함수를 이용

    return (
        <>
            {/* 상태변수를 사용(참조)할 때는 일반변수처럼 사용 */}
            <h1>{message}</h1>
            <button onClick={doEnterClick}>입장</button>
            <button onClick={doLeaveClick}>퇴장</button>
        </>
    );
}
export default Say;

App.js 파일에 Say 컴포넌트를 추가하겠습니다.

import Say from "./state/Say";

function App() {
  return (
    <>
      <Say />
    </>
  );
}
export default App;

여러 상태 변수 관리

useState를 여러 번 호출하여 각 상태 변수를 독립적으로 관리할 수 있습니다.

여러 개의 상태변수를 필요로 하는 경우 ⇒ useState 훅을 여러 개 사용 ⇒ Say 컴포넌트에 글자색을 변경하는 버튼을 추가 하겠습니다.

import { useState } from "react"; // useState 훅을 import

function Say() {
    // 상태 선언: message와 color
    const [message, setMessage] = useState(''); // 메시지 상태 (초기값: 빈 문자열)
    const [color, setColor] = useState('black'); // 텍스트 색상 상태 (초기값: 검정색)

    // 버튼 클릭 시 호출될 함수 정의
    const doEnterClick = () => setMessage('입장합니다.'); // "입장합니다." 메시지 설정
    const doLeaveClick = () => setMessage('퇴장합니다.'); // "퇴장합니다." 메시지 설정

    return (
        <>
            {/* 텍스트 출력 영역 */}
            {/* color 상태값을 동적으로 스타일로 적용 */}
            <h1 style={{ color }}>{message}</h1>

            {/* 메시지를 변경하는 버튼들 */}
            <button onClick={doEnterClick}>입장</button> {/* 클릭 시 "입장합니다."로 메시지 변경 */}
            <button onClick={doLeaveClick}>퇴장</button> {/* 클릭 시 "퇴장합니다."로 메시지 변경 */}

            {/* 텍스트 색상을 변경하는 버튼들 */}
            <button onClick={() => setColor('red')}>빨간색</button> {/* 클릭 시 텍스트 색상을 빨간색으로 변경 */}
            <button onClick={() => setColor('blue')}>파란색</button> {/* 클릭 시 텍스트 색상을 파란색으로 변경 */}
            <button onClick={() => setColor('green')}>초록색</button> {/* 클릭 시 텍스트 색상을 초록색으로 변경 */}
        </>
    );
}

export default Say; // 컴포넌트 내보내기

 

4. 상태 관리 시 주의사항

상태 변수는 불변성을 유지해야 함

상태가 배열이나 객체인 경우, 기존 상태를 직접 수정하면 안 됩니다. 대신 새로운 객체나 배열을 생성하여 업데이트해야 합니다.

// 객체의 사본 생성
const obj = { a: 1, b: 2, c: 3 }; // 원본 객체
// Spread 문법을 사용하여 obj의 모든 속성을 복사하고, 새로운 속성(key)을 추가
const newObj = { ...obj, key: newValue }; 
// 설명:
// 1. `...obj`는 obj의 모든 키-값 쌍을 펼쳐 새로운 객체에 복사함.
// 2. 이후에 작성된 `{ key: newValue }`는 기존 속성을 덮어쓰거나 새 속성을 추가함.
// 예) 원본: { a: 1, b: 2, c: 3 }, 결과: { a: 1, b: 2, c: 3, key: newValue }
// 객체의 불변성을 유지하며 새로운 객체를 생성.


// 배열의 사본 생성
const arr = [1, 2, 3, 4]; // 원본 배열

// Spread 문법을 사용하여 arr의 모든 요소를 복사하고, 새로운 요소(newItem)를 추가
const newArr = [...arr, newItem];
// 설명:
// 1. `...arr`는 원본 배열의 모든 요소를 복사함.
// 2. 뒤에 추가된 `newItem`은 복사된 배열의 마지막에 새로 추가됨.
// 예) 원본: [1, 2, 3, 4], 결과: [1, 2, 3, 4, newItem]
// 기존 배열(arr)을 변경하지 않고 새로운 배열을 생성.

// concat 메서드를 사용하여 새로운 배열을 생성
const newArr2 = arr.concat(100);
// 설명:
// 1. `concat` 메서드는 기존 배열을 수정하지 않고, 새로운 요소(100)를 추가한 새 배열을 반환.
// 예) 원본: [1, 2, 3, 4], 결과: [1, 2, 3, 4, 100]
// 배열의 불변성을 유지하는 안전한 방법.

// push 메서드를 사용한 경우
const newArr3 = arr.push(100);
// 설명:
// 1. `push` 메서드는 기존 배열(arr)을 직접 수정하여 요소(100)를 추가.
// 2. `arr` 자체가 변경되므로, React에서 상태 관리 시 불변성을 위반할 수 있음.
// 예) 원본: [1, 2, 3, 4], 변경된 원본: [1, 2, 3, 4, 100]
// 주의: push 메서드는 기존 배열을 변경하므로 상태 관리 시 사용하지 않는 것이 좋음.

잘못된 방식과 올바른 방식

// 잘못된 방식: 기존 배열을 직접 수정
const doAddNumberIncorrect = () => {
  arrData.push(Math.random());  // 기존 배열에 새로운 요소 추가 (직접 수정)
  setArrData(arrData);          // 상태 업데이트 함수 호출 (하지만 리렌더링되지 않음)
  
  // 문제점:
  // 1. React는 상태가 "새로운 값"으로 바뀌었을 때만 리렌더링을 발생시킴.
  //    여기서는 기존 배열을 직접 수정했기 때문에 React는 상태가 변경되지 않은 것으로 판단함.
  // 2. 상태값을 직접 변경하는 것은 React의 상태 관리 원칙을 어기는 행동임.
};

// 올바른 방식: 새로운 배열 생성 후 상태 업데이트
const doAddNumberRightway = () => {
  const newArr = [...arrData, Math.random()];  // 기존 배열을 복사하고 새 요소를 추가 (불변성 유지)
  setArrData(newArr);                          // 새로운 배열을 상태로 설정하여 리렌더링 발생

  // 설명:
  // 1. React의 상태는 불변성(immutable)을 유지해야 함. 즉, 상태를 직접 수정하지 않고 새 값을 만들어야 함.
  // 2. 새 배열을 생성하면 React는 상태가 변경된 것으로 인식하고 리렌더링을 트리거함.
  // 3. 기존 데이터를 안전하게 유지하며 새로운 요소를 추가할 수 있음.
};

 

5. 상태 관리 예제: 올바른 방식과 잘못된 방식을 통한 숫자 추가

아래는 상태 변수를 활용한 간단한 예제입니다.

import { useState } from "react"; // useState 훅 import

function App() {
  // 상태 선언: 배열을 저장할 상태 변수 arrData와 상태를 변경하는 함수 setArrData
  const [arrData, setArrData] = useState([]); // 초기값은 빈 배열

  // 잘못된 방식: 기존 배열을 직접 수정
  const doAddNumberIncorrect = () => {
    const rno = Math.random(); // 0과 1 사이의 랜덤 숫자 생성
    console.log(rno); // 생성된 랜덤 숫자 출력
    const newArr = arrData.push(rno); // 기존 배열(arrData)에 숫자를 추가 (push는 배열의 길이를 반환)
    console.log(newArr, arrData); // 반환 값(배열의 길이)과 기존 배열 출력
    setArrData(arrData); // 동일한 배열을 상태로 설정 (주소가 변경되지 않아 리렌더링되지 않음)
  };

  // 올바른 방식: 새로운 배열을 생성하여 상태를 업데이트
  const doAddNumberRightway = () => {
    const rno = Math.random(); // 0과 1 사이의 랜덤 숫자 생성
    console.log(rno); // 생성된 랜덤 숫자 출력
    const newArr = [...arrData, rno]; // 기존 배열을 복사하고 새 요소(rno)를 추가하여 새로운 배열 생성
    console.log(newArr); // 새로운 배열 출력
    setArrData(newArr); // 새로운 배열을 상태로 설정 (주소가 변경되어 리렌더링 발생)
  };

  return (
    <>
      {/* 잘못된 방식의 버튼 */}
      <button onClick={doAddNumberIncorrect}>숫자 추가 (잘못된 방식)</button>

      {/* 올바른 방식의 버튼 */}
      <button onClick={doAddNumberRightway}>숫자 추가 (올바른 방식)</button>

      {/* 배열 데이터를 리스트로 출력 */}
      <ul>
        {!!arrData && arrData.map((x, i) => <li key={i}>{x}</li>)} 
        {/* arrData가 존재하면 각 요소를 <li> 태그로 렌더링 */}
      </ul>
    </>
  );
}

export default App; // 컴포넌트 내보내기

 

 

 

*생성형 AI 활용한 클라우드&보안 전문가 양성캠프 과정의 교육내용 정리 자료입니다.