상태를 다루는 state 기초

sucream·2023년 1월 31일
0

react

목록 보기
3/9

State: 컴포넌트의 메모리

우리는 컴포넌트를 다루면서 input 필드 내용 업데이트, 버튼 클릭을 통한 화면 내 내용 전환 등 다양한 인터랙션을 통해 화면의 내용을 변경할 필요가 있다. 이를 위해 컴포넌트는 최근의 상태를 기억할 필요가 있는데, 리액트는 이를 위해 state라고 불리는 컴포넌트 특화 메모리를 제공한다.

그냥 변수를 사용하면 안돼?

그렇다. 리액트는 자바스크립트 라이브러리다. 그말은 우리는 자바스크립트 변수를 사용할 수 있다. 그렇다면 왜 state라고 하는 별도의 기능을 이용하는 것일까?

우선 아래 코드를 통해 확인해 보자.

const App = () => {
  let index = 0;

  const indexHandler = () => {
    index += 1;
    console.log(index);
  };

  return (
    <div>
      <div>현재 인덱스: {index}</div>
      <button onClick={indexHandler}>인덱스 증가</button>
    </div>
  );
};

export default App;

위 코드는 index라는 변수를 선언하고, 인덱스 증가 버튼을 클릭하면 onClick 이벤트에 의해 indexHandler 함수가 실행되어 index 변수가 증가되는 App이라는 간단한 컴포넌트다.

이 코드에서 버튼을 10번 눌러보겠다.

어떤가? 원하는대로 현재 인덱스가 10이 되었는가?
아니다. 그렇지 않다. 그대로 0이다.

왜 그대로?

그렇다면 우리가 정의한 indexHandler가 실행되지 않은 걸까? console.log를 확인해 보자.

콘솔을 확인해 보니 우리가 작성한 indexHandler는 정상적으로 작동해 index값이 증가한 것을 확인할 수 있다.

1. 렌더링
리액트는 컴포넌트를 여러번 렌더링할 수 있는데, 렌더링할 때 지역변수로 선언된 변수의 변경 사항을 고려하지 않는다고 한다.

2. 트리거
실제로는 이 이유때문인데, 리액트는 특정 조건이 트리거되면 컴포넌트를 다시 렌더링한다. 그러나 지역변수의 변경은 리액트에게 있어 재렌더링을 위한 트리거 역할을 하지 않는다. 따라서 변수가 변경되어도 리액트가 렌더링을 다시 하지 않아 화면상에 변수의 값이 바뀌어 보이지 않았던 것이다.

그럼 어떻게 해?

여러번 렌더링이 되어도 데이터를 유지할 수 있어야 하고, 새로운 데이터가 발생했으니 리액트에게 재렌더링을 트리거할 수 있어야 한다.
이를 위해 리액트는 useState라는 훅을 제공한다.

useState 훅

리액트가 제공하는 다양한 훅 중에서 가장 먼저, 가장 많이 만나게 되는 훅인 useState는 이름대로 state를 위한 훅이다. useState의 역할은 크게 두가지로 아래와 같다.

1. 렌더링 사이에 변수의 상태를 유지한다.

2. 변수를 업데이트하고 컴포넌트를 다시 렌더링하기위한 트리거를 제공한다.

useState는 const [stateVar, setStateVar] = useState(initialVal);의 형태로 사용한다.
initialVal은 변수의 초깃값을 나타낸다. 그리고 useState는 2개의 값을 리턴하는데, stateVar은 state가 나타내는 최신값을 가리키며, setStateVar은 해당 state의 값을 변경하기 위해 사용되는 함수다. 값이 변경되면 재렌더링을 위한 트리거가 발동되어 리액트는 새로운 값을 갱신하기 위해 컴포넌트 렌더링을 다시 한다. 그리고 setStateVar 함수의 이름 규칙이 있는데, 앞의 stateVar 변수명의 앞에 set을 붙여 해당 state를 변경함을 명확하게 나타낸다.

예시로 확인해 보자

위에서 다루던 예시를 변경해 보자.

import { useState } from 'react';  // useState 훅을 사용하기 위한 import

const App = () => {
  // useState를 이용하여 초깃값을 0으로 할당하였음
  // index라는 변수로 최신값에 접근할 수 있음
  // setIndex라는 함수로 index의 값을 변경할 수 있음
  // setIndex 함수 사용시 트리거가 발동되어 리액트가 컴포넌트를 재렌더링함
  const [index, setIndex] = useState(0);  // []는 배열 디스트럭팅이라는 문법임

  const indexHandler = () => {
    setIndex(index + 1)
    console.log(index);
  };

  return (
    <div>
      <div>현재 인덱스: {index}</div>
      <button onClick={indexHandler}>인덱스 증가</button>
    </div>
  );
};

export default App;

이제 우리가 예상한 대로 인덱스 값이 증가하고 있다!

State 좀 더 알아보기

재렌더링

앞에서 useState를 사용하면 컴포넌트 재렌더링을 통해 값이 변경되는 것을 확인할 수 있었다. 컴포넌트가 재렌더링된다는 말은 곧 컴포넌트 내 코드가 다시 평가된다는 뜻이다. 실제로 위 코드는 다음과 같이 실행된다.

1. 0으로 초기화된 index를 렌더링
2. setIndex(index + 1)을 통해 리액트에 index1임을 알린다.
3. 리액트가 컴포넌트를 다시 렌더링하지만 useState(0)이 0 아니라 리액트에 의해 관리되어 [1, setIndex]를 반환한다.

그렇다면 useState는 어떻게 변수를 관리할까?

우리가 사용하는 useStateconst [index, setIndex] = useState(0); 처럼 사용하는데, 자세히 보면 신기하지 않은가? 그렇다. useState는 index라는 변수에 대한 어떠한 정보도 가지고 있지 않다. 그런데 어떻게 index의 값이 몇인지 알고 재렌더링할 때 적절한 값을 반환할 수 있을까?

여기서 리액트에서 state 사용을 위한 한가지 중요한 규칙을 확인해 볼 수 있다. 우리는 state를 사용하기 위해서 반드시 컴포넌트의 최상위 레벨에 useState를 사용해야 한다. 이것이 리액트가 state를 관리하는 방법이다. 컴포넌트의 최상위 레벨에서 state가 정의되면 항상 같은 순서로 useState 훅이 호출되기 때문에 이를 관리할 수 있다는 것이다! 정말 단순해 보이지만 신기한 것 같다. 또한 대부분의 linter가 최상위 레벨이 아닌 곳에서의 useState 사용을 탐지하기 때문에 괜찮다고 한다.
내부적으로 리액트는 모든 컴포넌트에 대한 state 배열을 가진다고 한다.

아래 코드는 useState의 동작 방식 이해를 위한 예제 코드 및 결과이다. 확인해 보면 도움이 될 것 같다.

State는 격리되어있다

우리가 컴포넌트를 사용하는 이유중 가장 큰 이유중 하나는 분명 재사용성일 것 이다. 만약 state를 사용하는 컴포넌트를 여러곳에서 사용했을 때 모든 컴포넌트의 state가 같은 값을 가진다고 생각해 보라. 얼마나 끔찍한가... 다행이도 리액트 컴포넌트는 2개 혹은 n개 사용해도 각 컴포넌트 내 state는 공유되지 않는다. 이를 통해 우리는 편하게 컴포넌트를 재사용할 수 있다. 역시 컴포넌트는 함수라고 생각하면 편한 것 같다.

// 자식 컴포넌트
import { useState } from 'react';

const Child = () => {
  const [index, setIndex] = useState(0);

  const indexHandler = () => {
    setIndex(index + 1)
    console.log(index);
  };

  return (
    <div>
      <div>현재 인덱스: {index}</div>
      <button onClick={indexHandler}>인덱스 증가</button>
    </div>
  );
};

export default Child;
// 부모 컴포넌트
import Child from "./Child";

const Parent = () => {
  return (
  	<div>
      <Child />
      <Child />
    </div>
  );
};
export default Parent;

만약 두 Child 컴포넌트의 state를 동기화 하려면 어떻게 해야할까? 다음에는 state와 관련된 더욱 다양한 부분에 대해 얘기해 보도록 하자.

Reference

profile
작은 오븐의 작은 빵

0개의 댓글