state는 리액트에서 변수 같은것인데, State를 바꾸면 리액트가 알아서 화면을 새로 렌더링 해준다.

리액트에서 state를 만들고, state를 바꾸기 위해서는 일단 useState라는 함수를 활용해야 한다.

//App.js
import { useState } from 'react';

// ...

  const [num, setNum] = useState(1);

// ...

보통 이렇게 Destructuring 문법으로 작성하는데 useState 함수가 초기값을 아규먼트로 받고 그에 따른 실행 결과로 요소 2개를 가진 배열의 형태로 리턴을 하기 때문이다.

이때 첫 번째 요소가 바로 state이고, 두 번째 요소가 이 state를 바꾸는 setter 함수인데, 참고로 위 코드에서도 볼 수 있듯 첫 번째 변수는 원하는 state의 이름(num)을 지어주고, 두 번째 변수에는 state 이름 앞에 set을 붙인 다음 카멜 케이스로 이름을 지어주는 것(setNum)이 일반적이다.

state는 변수에 새로운 값을 할당하는 방식으로 변경하는 것이 아니라 반드시 setter 함수를 활용해야 하는데 setter 함수는 호출할 때 전달하는 아규먼트 값으로 state 값을 변경해 준다. (그래서 const 키워드로 state를 만든다.)

그래서 아래 코드처럼 setter 함수를 활용해서 이벤트 핸들러를 등록해두면, 이벤트가 발생할 때마다 상태가 변하면서 화면이 새로 그려지게 된다.

//App.js
import { useState } from 'react';
import Button from './Button';
import Dice from './Dice';

function App() {
  const [num, setNum] = useState(1);

  const handleRollClick = () => {
    setNum(3); // num state를 3으로 변경!
  };

  const handleClearClick = () => {
    setNum(1); // num state를 1로 변경!
  };

  return (
    <div>
      <Button onClick={handleRollClick}>던지기</Button>
      <Button onClick={handleClearClick}>처음부터</Button>
      <Dice color="red" num={num} />
    </div>
  );
}

export default App;
//Button.js
import { children } from "react";

function Button({ children, onClick }) {
  return <button onClick={onClick}>{children}</button>;
}

export default Button;
//App.js
import { useState } from "react";
import Button from "./Button";
import Dice from "./Dice";

function random(n) { //파라미터 n으로 숫자값을 전달받아서
  return Math.ceil(Math.random() * n); //1부터 n까지의 랜덤한 정수를 반환하는 함수
}

function App() {
  const [num, setNum] = useState(1);

  const handleRollClick = () => {
    const nextNum = random(6); //random6을 할당해준 다음 이 값으로 setNum 함수를 호출하면
    setNum(nextNum); //던지기 버튼을 누를 때마다 랜덤한 숫자로 num State가 변경
  };

  const handleClearClick = () => {
    setNum(1);
  };

  return (
    <div>
      <div>
        <Button onClick={handleRollClick}>던지기</Button>
        <Button onClick={handleClearClick}>처음부터</Button>
      </div>
      <Dice color="red" num={num} />
    </div>
  );
}

export default App;

참조형 State

자바스크립트의 자료형은 크게 기본형(Primitive type)과 참조형(Reference type)로 나눌 수 있다. 특히 참조형 값들은 조금 독특한 특성을 가지고 있어서 변수로 다룰 때도 조금 주의해야 할 부분들이 있었는데 state를 활용할 때도 마찬가지이다.

// ... 

  const [gameHistory, setGameHistory] = useState([]);

  const handleRollClick = () => {
    const nextNum = random(6);
    gameHistory.push(nextNum);
    setGameHistory(gameHistory); // state가 제대로 변경되지 않는다!
  };

// ...

위 코드에서 볼 수 있듯 배열 값을 가진 gameHistory에 push 메소드를 이용해서 배열의 값을 변경한 다음, 변경된 배열을 setter 함수로 state를 변경하려고 하면 코드가 제대로 동작하지 않는다.

gameHistory state는 배열 값 자체를 가지고 있는 게 아니라 그 배열의 주소값을 참조하고 있기 때문에 push 메소드로 배열 안에 요소를 변경했다고 하더라도 결과적으로 참조하는 배열의 주소값은 변경된 것이 아니게 된다.

리액트 입장에서도 state 값이 바뀌어야 새롭게 화면을 렌더하는데 아무리 새로운 요소가 추가된 배열을 setter 함수에 담았다고 하더라도 요소가 추가되기 전의 배열과 추가된 이후의 배열의 주소값은 계속해서 같기 때문에 state가 변경되었다고 판단하지 않는 것이다.

그래서 배열이나 객체같은 참조형 타입 state를 변경할 때는 아예 전체를 새로 만든다고 생각하는게 좋다. 가장 간단한 방법은 Spread 문법(...)을 활용하는 것이다.

import { useState } from "react";
import Button from "./Button";
import Dice from "./Dice";

function random(n) {
  return Math.ceil(Math.random() * n);
}

function App() {
  const [num, setNum] = useState(1);
  const [sum, setSum] = useState(0);
  const [gameHistory, setGameHistory] = useState([]); //빈 배열열

  const handleRollClick = () => {
    const nextNum = random(6);
    setNum(nextNum);
    setSum(sum + nextNum); //던지기 버튼을 누를때마다 새로운 주사위 값이 sum State에 더해지게 된다.
    // gameHistory.push(nextNum); //gameHistory가 배열이니까 push 메소드로 nextNum을 추가한 다음
    // setGameHistory(gameHistory); //setter 함수로 새 값이 추가된 gameHistory State를 전달
    setGameHistory([...gameHistory, nextNum]);
  };

  const handleClearClick = () => {
    setNum(1);
    setSum(0);
    setGameHistory([]);
  };

  return (
    <div>
      <div>
        <Button onClick={handleRollClick}>던지기</Button>
        <Button onClick={handleClearClick}>처음부터</Button>
      </div>
      <div>
        <h2>나</h2>
        <Dice color="blue" num={num} />
        <h2>총점</h2>
        <p>{sum}</p>
        <h2>기록</h2>
        <p>{gameHistory.join(", ")}</p>
      </div>
    </div>
  );
}
//join메소드는 아규먼트로 전달한 값을 배열의 각 요소들 사이사이에 넣어서 결과적으로 하나의 문자열로 만들어주는 메소드이다.

export default App;

이 참조형 state의 특성을 이해하지 못하면, 간혹 state가 제대로 변경되지 않는 버그가 발생했을 때 원인을 제대로 찾지 못하는 경우가 발생할 수도 있기 때문에 참조형 state를 활용할 땐 반드시 새로운 값을 만들어서 state를 변경해야 한다.

'코린이 개념잡기 > React' 카테고리의 다른 글

리액트 렌더링  (0) 2025.01.03
리액트 디자인 적용하는 방법 +🍯  (3) 2025.01.03
Props와 Children  (1) 2025.01.03
컴포넌트 문법  (0) 2025.01.03
JSX 문법  (0) 2025.01.03

+ Recent posts