리액트

[React] State 자세히 알아보기

코딩하는둥이 2025. 4. 3. 10:53

State는 왜 사용할까?

 

Counter 함수 컴포넌트는 함수이기 때문에 호출 시에 return문으로 실행되고 끝나기만 합니다.

import React from "react";
    export default function Counter() { I
    let count = 0;
    return (
        <div>
            <button onClick={() => (count += 1)}>+1</button>
            <br />
            Counter: {count}
        </div>
    );
}

 

그래서 버튼을 클릭해도 숫자가 바뀌지 않습니다. 

 

 

함수 컴포넌트에서 내부적으로 상태를 관리하는 useState가 필요합니다.

상태값을 관리한다는 뜻은 상태값을 변경이 가능하고 컴퍼넌트가 렌더링이 되도록 합니다.

 

useState 배열을 리턴해줍니다.

useState가 리턴하는 첫번째 값은 관리하고 있는 값입니다.

useState가 리턴하는 두번째 값은 값을 변경할 때 쓰는 함수입니다.

 

import React, { useState } from “react”; 

const [state, setState] = useState();

 

 

useState를 사용해서 다시 만들어보겠습니다.

setCount() ()안에는 업데이트할 값을 넣어주면 됩니다.

import React from "react";
    export default function Counter() { I
    const [count, setCount] = useState(0);
    return (
        <div>
            <button onClick={() => (setCount(count += 1)}>+1</button>
            <br />
            Counter: {count}
        </div>
    );
}

 

값이 증가되는 것을 알수 있다.

 

불변성

 : 변하지 않는 성질 

 => 프로그래밍에서 불변성을 지킨다는 말은 모리 영역의 값을 직접적으로 변경하지 않는다는 의미가 있습니다.

 

 

setState()의 이전의 값과 현재의 값이 다르면 컴포넌트는 re-rendering됩니다.

만약에 값이 같다면 불변성을 지키지 않고 메모리 영역의 값을 직접 변경하면 리액트는 state가 바뀐다고 인지를 못합니다.

그 이유는 리액트는 이전 state과 이후 state를 비교할 떄,  얕은 비교을 하기 때문입니다.

 

 

데이터 타입은 원시타입과 참조타입이 있습니다.

원시타입 참조타입
boolean, number, string object
불변성을 가지고 있습니다. 불변성을 가지고 있지 않습니다.
변수에 원시 타입의 값을 할당 변수에 참조 타입의 값을 할당하면, 메모리 값이 담긴 주소가 저장됩니다.

 

 

먼저 원시타입부터 확인해보겠습니다. 

버튼을 통해 count가 보였다가 안 보였다가 하는 기능을 추가합니다.

버튼을 클릭 시 useState가 false가 되면서 show에는 counter가 안보이게 되고 다시 클릭하면 true가 되서 보이게 됩니다.

import React from "react";
    export default function Counter() { I
    const [count, setCount] = useState(0);
    const [show, setShow] = useState(true);
    return (
        <div>
            <button onClick={() => (setCount(count += 1)}>+1</button>
            <button onClick={() => (setshow(!show)}>show and Hide</button>
            <br />
            {show && `Counter: {count}`)
        </div>
    );
}

 

아래 이미지를 보면 알다시피 count가 없어진 걸 확인할 수 있습니다.

 

이제 버튼 하나를 더 만들어서 랜덤 연산자로 계산할 수 있도록 만듭니다.

기존 코드에 연산자 배열(operators)과 상태(operator)를 추가하여 버튼 클릭 시 count 값을 동적으로 계산하는 기능을 구현했습니다.

import React, { useState } from "react";

export default function Counter() {
    const [count, setCount] = useState(0);
    const [show, setShow] = useState(true);

    const operators = ["+", "-", "*", "/"];
    const [operator, setOperator] = useState(operators);

    return (
        <div>
            <button onClick={() => {
                let result;
                if (operator === "+") result = count + 1;
                if (operator === "-") result = count - 1;
                if (operator === "*") result = count * 1;
                if (operator === "/") result = count / 1;
                setCount(result);
            }}>
                {operator}1
            </button>

            {/* Show/Hide 버튼 */}
            <button onClick={() => setShow(!show)}>
                Show and Hide
            </button>
            <button onClick={() => {
                const idx = Math.floor(Math.random() * operators.length);
                setOperator(operators[idx]);
            }}>
                Change Operator
            </button>
            <br />
            {show && `Counter: ${count}`}
        </div>
    );
}

 

 

 

 

이제 참조타입에 대해서 알아보겠습니다.

객체나 배열은 직접 수정하면 안되고 setState로 새로운 객체 배열을 할당해야합니다. 

 import React, { useState } from “react”; 
 
const [ array, setArray ] = useState([“a”, “b”, “c”, “d”])

 

사용할 때는 ...를 사용해서 배열을 나열 시킨 다음에 사용할 수 있습니다.

새로운 배열을 반환하는 작업이 필요합니다. 

setArray([…array, newItem])
setArray(array.filter(arr => {}

 

기존의 개별 상태 변수(count, show, operator)를 하나의 객체(info)로 통합해 상태 관리 방식을 단순화했고 상태 업데이트 시 스프레드 연산자(...info)를 사용해 기존 상태를 유지하면서 필요한 값만 변경하도록 수정했습니다.

import React, { useState } from "react";

export default function Counter() {
    const operators = ["+", "-", "*", "/"];
    const [info, setInfo] = useState({
        count: 0,
        show: true, 
        operator: operators[0]
    });

    return (
        <div>
            <button onClick={() => {
                let result;
                switch(info.operator) {
                    case "+": result = info.count + 1; break;
                    case "-": result = info.count - 1; break;
                    case "*": result = info.count * 1; break;
                    case "/": result = info.count / 1; break;
                }
                setInfo({...info, count: result});
            }}>
                {info.operator}1
            </button>

            <button onClick={() => setInfo({...info, show: !info.show})}>
                Show and Hide
            </button>

            <button onClick={() => {
                const idx = Math.floor(Math.random() * operators.length);
                setInfo({...info, operator: operators[idx]}); 
            }}>
                Change Operator
            </button>

            <br />
            {info.show && `Counter: ${info.count}`}
        </div>
    );
}

 

props와 state 차이점

props state
부모 컴포넌트가 자식 컴포넌트에게 전달하는 값 자신(컴포넌트)이 스스로 관리하는 상태값
값을 자신(자식 컴포넌트)이 변경 불가능 값이 자신이 변경 가능
props를 통해 값을 내려받거나, 자신이 관리하고 있는 state가 변경되면 컴포넌트 렌더링이 발생합니다.

 

 

state 사용 시 주의할 점

 

1. 직접 state를 수정하면 안됩니다.

 아래 코드처럼 한다면 다시 렌더링하지 않습니다.

this.state.comment = 'hello';

 

아래 코드처럼 setState()를 사용합니다.

this.setState({comment = 'hello'});

 

this.state를 지정할 수 있는 유일한 공간은 constructor입니다.

 

2. state 업데이트는 비동기적일 수도 있습니다.

setState() 호출을 단일 업데이트로 한꺼번에 처리할 수 있습니다. 그 이유는 기존에 있는 리액트 엘리먼트와 setState을 실행하면서 바뀔 리액트 앨리먼트 트리를 비교를 해서 달라지는 부분이 있으면 DOM이 업데이트하는 방식입니다. 그런 방식이라서 복잡하기 때문에 하나 실행할때마다 업데이트가 아니라 단일 업데이트로 한번에 처리합니다.

 

3. state 업데이트는 병합합니다.

setState() 호출할 때 react에서 제공한 객체를 현재 state로 변합합니다.

 

아래 코드를 통해 알아보겠습니다.

 

multiplyBy2AndAddBy1를 클릭하면 *2가 될 것 같지만, +1이 됩니다. 그이유는 병합이 되면서 비동기로 작동되었기 때문입니다. multiplyBy2AndAddBy1는  setNumber((number) => number + 1);를 실행하고  setNumber((number) => number * 2)것이 아닌 Object.assign({number, number: number *2, number: number + 1});처럼 실행이 되어 3, 6, 4 값이 나오는데 같은 변수명은 number이기 때문에 마지막으로 4가 나오게 됩니다.

import React, { useState } from "react";

export default function App() {
  const [number, setNumber] = useState(1);

  const add = () => setNumber(() => number + 1);
  const subtract = () => setNumber(() => number - 1);
  const multiplyBy2 = () => setNumber(() => number * 2);
  const multiplyBy2AndAddBy1 = () => {
    multiplyBy2();
    add();
  };

  return (
    <div>
      <h1>Number : {number}</h1>
      <div>
        <button onClick={add}>+ 1</button>
        <button onClick={subtract}>- 1</button>
        <button onClick={multiplyBy2}>*2</button>
        <button onClick={multiplyBy2AndAddBy1}>*2 + 1</button>
      </div>
    </div>
  );
}

 

원하는 대로 값이 나오게 하려면 현재의 number를 넘겨주면 됩니다.

import React, { useState } from "react";

export default function App() {
  const [number, setNumber] = useState(1);

  const add = () => setNumber((number) => number + 1);
  const subtract = () => setNumber((number) => number - 1);
  const multiplyBy2 = () => setNumber((number) => number * 2);
  const multiplyBy2AndAddBy1 = () => {
    multiplyBy2();
    add();
  };

  return (
    <div>
      <h1>Number : {number}</h1>
      <div>
        <button onClick={add}>+ 1</button>
        <button onClick={subtract}>- 1</button>
        <button onClick={multiplyBy2}>*2</button>
        <button onClick={multiplyBy2AndAddBy1}>*2 + 1</button>
      </div>
    </div>
  );
}

 

 

4. 단방향 데이터 흐름

컴포넌트는 자신의 state를 자식 컴포넌트에 props로 전달할 수 있습니다.

 

모든 state는 항상 특정한 컴포넌트가 소유하고 있으며 그 state로부터 파생된 UI 또는 데이터는 오직 트리구조에서 자신의 아래에 있는 컴포넌트만 영향을 미칩니다.

 

 

state 끌어올리기 

React에서 여러 컴포넌트가 동일한 데이터를 공유해야 할 때 사용하는 패턴입니다.

 

Calculator.js

 부모 컴포넌트로, 섭씨와 화씨 입력 필드 상태를 관리합니다.

handleTemperatureChange(object)를 통해 자식 컴포넌트에서 호출되어 상태를 업데이트합니다.

import React, { useState } from "react";
import BoilingVerdict from "./BoilingVerdict";
import TemperatureInput from "./TemperatureInput";

export default function Calculator() {
  const [state, setState] = useState({
    scale: "c",
    temperature: "",
  });

  const handleTemperatureChange = (obj) => {
    // obj.scale: 수정된 단위 ("c" 또는 "f")
    // obj.temperature: 수정된 온도 값
    setState({ scale: obj.scale, temperature: obj.temperature });
  };

  function toCelsius(fahrenheit) {
    return ((fahrenheit - 32) * 5) / 9;
  }

  function toFahrenheit(celsius) {
    return (celsius * 9) / 5 + 32;
  }

  function tryConvert(temperature, convert) {
    const input = parseFloat(temperature);
    if (Number.isNaN(input)) {
      return "";
    }
    const output = convert(input);
    const rounded = Math.round(output * 1000) / 1000;
    return rounded.toString();
  }

  const { scale, temperature } = state;
  
  // 섭씨 및 화씨 값 계산
  const celsius =
    scale === "f" ? tryConvert(temperature, toCelsius) : temperature;
  const fahrenheit =
    scale === "c" ? tryConvert(temperature, toFahrenheit) : temperature;

  return (
    <>
      <TemperatureInput
        scale="c"
        temperature={celsius}
        onTemperatureChange={(temp) =>
          handleTemperatureChange({ scale: "c", temperature: temp })
        }
      />
      <TemperatureInput
        scale="f"
        temperature={fahrenheit}
        onTemperatureChange={(temp) =>
          handleTemperatureChange({ scale: "f", temperature: temp })
        }
      />
      <BoilingVerdict scale="c" temperature={parseFloat(celsius)} />
    </>
  );
}

 

 

TemperatureInput.js

입력필드 컴포넌트오, 사용자가 온도를 입력할 수 있습니다. 부모로 부터 scale, temperature, onTemperatureChange props를 받습니다.

import React from "react";

const scaleNames = {
  c: "Celsius", // 섭씨
  f: "Fahrenheit", // 화씨
};

export default function TemperatureInput(props) {
  
  function handleChange(e) {
    props.onTemperatureChange(e.target.value); // 부모에 값 전달
  }

  const scale = scaleNames[props.scale];

  return (
    <fieldset>
      <legend>Enter temperature in {scale}:</legend>
      <input value={props.temperature} onChange={handleChange} />
    </fieldset>
  );
}

 

BoilingVerdict.js

물이 끓는지 여부를 판단하여 메게지를 출력합니다.

import React from "react";

export default function BoilingVerdict(props) {
  
  if (props.scale === "c") {
    if (props.temperature >= 100) {
      return <p>The water would boil.</p>;
    }
    return <p>The water would not boil.</p>;
  }

  if (props.temperature >= 212) {
    return <p>The water would boil.</p>;
  }
  
  return <p>The water would not boil.</p>;
}

 

Calculator.js가 공통 부모로서 두 개의 입력 필드(TemperatureInput)의 상태를 관리합니다. 각 TemperatureInput은 자신의 값을 변경할 때 부모의 상태 변경 함수(handleTemperatureChange)를 호출합니다. 부모는 새 값을 기반으로 다른 입력 필드 값을 계산하고 다시 자식에게 전달하면서 State가 끌어올라가는 걸 확인할 수 있습니다.