본문 바로가기

나야, 리액트 스터디

[week3] useState,,, 제대로 알고 계신가요?!

안녕하세요 웹파트 YB 김다현입니다.

 

이번 3주차에서는 useState에 대해 다뤄보려고 합니다. 리액트로 개발을 하다보면 useState를 수도 없이 쓰게 되는데요!

생각해보니까 그냥 기계적으로 쓰기만 했지 뭔가 이게 뭐 어떻게 작동되는 건지,,, 그런 건 아예 모르는 상태더라구요.

그리고 우선 저는 딱 읽자마자 '일반 변수로 충분하지 않은 경우' 라는 제목부터 이해가 잘 안 갔기 때문에 ㅎㅎ...

지금이 리액트의 핵심 개념 중 하나인 상태 관리의 의미와 역할을 이해할 수 있는 기회라고 생각해 이 부분에 대해 작성하게 되었습니다.

오늘도 공식문서를 쉽게! 최대한 누구나 이해할 수 있게! 아티클을 작성해볼게요 😅

 

다 이해하고 나면 되게 당연한 것 같기도 하고 후루룩 넘길 수 있는 챕터였던 것 같은데 저는 처음 읽었을 때는 뭐라는 거지... 했어요

 

변경사항을,, 기억해주는,, useState

일반 변수로 충분하지 않은 경우 

https://codesandbox.io/p/sandbox/d6z6j2?file=%2Fsrc%2FApp.js

 

현재 이 코드가 어떻게 동작하는지 알아볼까요?

지금 이 코드는 "Next" 버튼을 누를 때마다 다음 조각상을 보여주려고 작성된 코드일 거예요.

하지만 막상 실행해보면 버튼을 눌러도 이미지가 바뀌지 않는 문제가 발생합니다. 그 이유를 살펴보면서 왜 일반 변수로는 원하는 기능을 구현할 수 없는지 알아볼게요!

 

1️⃣ index 변수로는 왜 조각상이 바뀌지 않을까요?

코드에서 let index = 0 이라고 선언했죠? 이 변수는 매번 새로 그려질 때마다 0으로 초기화 됩니다.

버튼을 누르면 index가 1로 바뀌긴 하지만, 리액트가 새로 그려질 때 index는 다시 0으로 돌아가버리는 거예요!

그래서 버튼을 클릭해도 항상 첫 번째 조각상만 보여지게 됩니다.

 

2️⃣ index가 바뀌어도 리액트가 인식하지 못해요

리액트는 ‘이 컴포넌트가 업데이트할 필요가 있어!’라고 인식해야만 다시 화면을 그려줘요.

그런데 index라는 일반 변수를 바꾸기만 해서는 리액트가 "어? 뭔가 바뀌었네? 다시 그려야지!"라고 알 수 없어요.

리액트는 index가 바뀌었는지 모르는 상태라 화면을 바꿀 생각을 하지 않는 것!

 

그럼 어떡해? useState로 리액트에게 알려주기!

리액트는 컴포넌트가 상태를 useState 훅으로 관리하면 그 상태가 바뀔 때마다 알아차리고 새로 그려줄 준비를 해요.

이걸 이용해서 우리가 클릭한 결과를 리액트가 기억하도록 바꿔볼 수 있어요.

 

 

공식 문서에서는 위 사진처럼 useState의 두 기능을 설명하고 있는데요. 풀어서 설명하자면,

 

1. state 변수는 리렌더링이 되더라도 값이 유지되도록 도와주는 역할을 해요.

렌더링이 일어날 때마다 index가 0으로 돌아가는 게 아니라 업데이트된 값을 계속 기억하게 해줍니다.

 

2. state setter 함수는 state를 변경할 뿐 아니라 리액트에게 "이제 컴포넌트를 다시 그려줘!" 라고 신호를 보내기 때문에 변경 사항이 화면에 반영됩니다.

 

이 두 기능 덕분에 컴포넌트는 사용자와의 상호작용에 따라 변화를 기억하고 적절히 업데이트할 수 있어요.

그럼 이제 useState로 state 변수를 추가해 보면서 이 기능들이 어떻게 동작하는지 자세히 살펴볼까요?

state 변수 추가하기

1. useState로 state 변수 추가하기

 

먼저 useState 훅을 사용하기 위해 파일 상단에서 가져옵니다.

import { useState } from 'react';

 

그 다음 기존의 일반 변수인 let index = 0; 부분을 아래와 같이 useState로 바꿔볼게요.

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

 

이 코드에서 index는 state 변수이고, setIndex는 이 변수를 업데이트하는 setter 함수예요.

 

index와 setIndex가 정확히 무슨 역할을 하고 있지?

index -> 현재 조각상이 어디에 있는지를 저장하는 변수예요. 이 값은 우리가 현재 보여주고 있는 조각상의 위치를 기억해줘요.

setIndex -> index 값을 바꾸는 역할을 하는 함수입니다. setIndex를 통해서만 index 값을 변경할 수 있고 이 함수를 써서 index가 바뀌면 리액트가 화면을 다시 그려주게 돼요.

 

둘이 왜 필요한 거지?

단순히 index만 있으면 값은 기억하지만 리액트가 변화된 걸 몰라서 화면을 업데이트 하지 않아요.

반대로 setIndex를 사용하면 리액트가 변화가 생겼다고 인식하고 새 값을 기반으로 화면을 다시 그립니다.

 

생각해보면 index는 값 자체를 기억하는 역할이고 setIndex는 리액트에게 '이 값을 바꾸고 새로 화면을 그려줘!' 라고 신호를 주는 역할을 한다고 보면 돼요. 

 

그리고 공식 문서에서도 잠깐 다루고 있길래 잠깐 배열 구조 분해에 대해 언급하고 넘어가자면!

배열 구조 분해란?

배열 구조 분해는 배열의 요소들을 각각 변수에 쉽게 할당할 수 있도록 도와주는 문법이에요. 예를 들어, [10, 20]이라는 배열이 있다면, 배열 구조 분해를 통해 각 요소를 간편하게 변수에 나눠 담을 수 있습니다.

const [a, b] = [10, 20];
console.log(a); // 10
console.log(b); // 20

 

이 코드에서 [a, b] = [10, 20] 부분이 바로 배열 구조 분해입니다. 배열의 첫 번째 요소가 a에, 두 번째 요소가 b에 할당된 걸 볼 수 있어요!

 

리액트의 useState와 배열 구조 분해

 

리액트의 useState 훅은 아까 말한 것처럼 [state 변수, setter 함수] 형태로 두 가지 값을 배열로 반환해요. 지금 useState(0)은 [현재 state 값, state를 업데이트하는 함수] 형태의 배열을 반환합니다. 이 두 가지 값을 별도의 변수로 사용하려면 배열 구조 분해가 아주 유용해요.

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

 

배열 구조 분해를 사용해서 useState가 반환하는 두 가지 값을 index와 setIndex라는 이름으로 각각 꺼내 쓰는 거예요. 이렇게 하면 index로 현재 값을 추적하고, setIndex로 그 값을 업데이트할 수 있게 됩니다.

 

 


 

2. state 변수와 setter 함수로 클릭 시 index 업데이트하기

 

이제 handleClick 함수에서 setIndex를 사용해 index를 업데이트해 볼게요.

function handleClick() {
  setIndex(index + 1);
}

 

이렇게 setIndex로 값을 업데이트하면 리액트가 자동으로 컴포넌트를 다시 렌더링해서 index의 변화가 화면에 반영됩니다. 이제 버튼을 누를 때마다 다음 조각상이 보이게 되는 거죠!

 

리액트의 첫 번째 훅, useState

리액트에서 useState는 아주 중요한 첫 번째 훅이에요. 이란 리액트에서 use로 시작하는 특별한 함수들을 말합니다. 이 훅들을 통해 리액트는 다양한 기능을 컴포넌트에 연결할 수 있어요. 그리고 useState는 그중에서도 컴포넌트가 특정 값을 기억할 수 있게 해주는 기능이에요.

 

주의! 훅의 사용 규칙

 

훅을 사용할 때는 꼭 기억해야 할 규칙이 하나 있는데 훅은 컴포넌트의 최상위 수준에서만 사용해야 한다는 점이에요. 조건문이나 반복문 안에서 훅을 호출하면 안 됩니다!

훅은 한 번 선언하면 언제나 필요한 기능으로서 컴포넌트 전체에서 사용할 수 있어야 하기 때문이에요. 마치 파일 상단에서 import 문으로 모듈을 불러오는 것처럼 컴포넌트 최상위에서 훅을 선언해야 합니다.

 

리액트한테 기억하라고 말하는 법, useState 해부하기

useState를 호출하는 것은 리액트에게 “이 값을 기억해줘!”라고 요청하는 거예요.

예를 들어 index라는 값을 기억하고 싶다면 아래와 같이 useState를 선언할 수 있습니다.

const [index, setIndex] = useState(0);​

 

이 코드에서는 index라는 state 변수setIndex라는 state 변경 함수가 생성됩니다. 계속 말한 것처럼 index는 현재 값을 저장하고 있고 setIndex는 이 값을 변경할 때 사용하는 함수겠죠?!

 

중요! 변수 이름의 규칙

일반적으로 state 변수 이름은 원하는 대로 정할 수 있지만 const [something, setSomething]과 같이 작성하는 것이 관례예요.

이 규칙을 따르면 프로젝트 전반에서 일관되게 변수 이름을 사용할 수 있고 다른 개발자들도 쉽게 이해할 수 있으니 굳이 다른 이름 짓지 말고 이렇게 사용하기~

 

useState의 유일한 인수?

useState에서 유일한 인수는 state 변수의 초깃값이에요. 예를 들어 useState(0)은 index의 초깃값을 0으로 설정해 주는 거예요. 이렇게 초깃값을 설정해두면 리액트가 컴포넌트를 처음 렌더링할 때 그 값을 기억하기 시작한답니다!

 

useState가 작동하는 방식

그럼 리액트가 useState로 값을 어떻게 기억하고 업데이트하는지 알아볼까요?

예제를 통해 한 단계씩 설명해볼게요!

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

 

1. 첫번째 렌더링

컴포넌트가 처음 렌더링되면 useState(0)이 [0, setIndex] 배열을 반환합니다. 이때 리액트는 index의 값을 0으로 기억해요.

 

2. 사용자가 값을 업데이트 할 때

사용자가 버튼을 클릭해서 setIndex(index + 1)을 호출했다고 해볼게요. 이 함수 호출로 index 값이 1로 바뀌게 되죠. 그러면 리액트는 "어? index가 1로 바뀌었네!"라고 인식하고 이 변화를 반영하기 위해 컴포넌트를 다시 렌더링합니다.

 

3. 두번째 렌더링
컴포넌트가 두 번째로 렌더링될 때 리액트는 useState(0)을 다시 보지만 이미 index가 1로 업데이트된 것을 기억하고 있기 때문에 [1, setIndex]를 반환합니다. 이렇게 리액트는 변화를 감지하고 이전 상태를 유지하면서 새로운 렌더링을 해주는 거예요.

 

이후에도 setIndex가 호출될 때마다 리액트는 index의 최신 값을 기억하고 계속해서 컴포넌트를 렌더링해 주게 됩니다. 이렇게 state 변수와 state setter 함수가 컴포넌트의 기억과 업데이트 기능을 담당하게 되는 거예요!

 

여러 state 변수 사용하기

리액트 컴포넌트에는 필요한 만큼의 state 변수를 자유롭게 추가할 수 있어요.

아래 코드를 보면 index와 showMore라는 두 개의 state 변수가 사용되고 있습니다.

 

index는 현재 보여주는 조각상의 위치를 저장하는 숫자형 state 변수이고,

showMore는 “Show details” 버튼을 클릭할 때 세부 정보를 보여줄지 말지를 결정하는 불리언형 state 변수예요.

 

import { useState } from 'react';
import { sculptureList } from './data.js';

export default function Gallery() {
  const [index, setIndex] = useState(0); // 현재 조각상의 위치를 기억하는 state
  const [showMore, setShowMore] = useState(false); // 세부 정보를 표시할지 여부를 기억하는 state

  function handleNextClick() {
    setIndex(index + 1); // Next 버튼 클릭 시 index 증가
  }

  function handleMoreClick() {
    setShowMore(!showMore); // Show/Hide 토글
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleNextClick}>
        Next
      </button>
      <h2>
        <i>{sculpture.name}</i> 
        by {sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} of {sculptureList.length})
      </h3>
      <button onClick={handleMoreClick}>
        {showMore ? 'Hide' : 'Show'} details
      </button>
      {showMore && <p>{sculpture.description}</p>}
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
    </>
  );
}
 

언제 여러 개의 state 변수로 나누는 게 좋을까?

위 코드처럼 index와 showMore은 서로 독립적으로 동작하죠. 하나는 버튼을 클릭할 때 조각상을 변경하고 다른 하나는 세부 정보를 보여줄지 결정합니다. 서로 연관성이 없는 데이터는 각각 별도의 state 변수로 관리하는 것이 좋습니다. 이렇게 하면 코드가 더 읽기 쉽고 각각의 변수를 독립적으로 업데이트할 수 있어요!

하나의 객체로 합쳐야 할 때

하지만! 때로는 여러 state 변수를 하나로 합쳐서 관리하는 것이 더 편리할 수 있어요.

예를 들어 폼 필드처럼 서로 밀접하게 관련된 값들이 많을 때는 개별 state 변수 대신 하나의 객체로 통합해서 사용하는 것이 좋습니다.

사용자의 이름과 이메일을 입력받는 폼이라면 각 필드마다 따로 state 변수를 선언하기보다는 객체 형태의 state를 하나 선언하여 한 번에 관리하는 편이 효율적이랍니다,,,!

 
const [formData, setFormData] = useState({ name: '', email: '' });

 

이렇게 하면 객체의 속성 하나를 업데이트할 때도 다른 값들이 함께 관리되기 때문에 더 간편하게 관리할 수 있어요.

 

근데 리액트는 어떤 state를 반환할지 어떻게 아는 걸까?

이 부분은 공식 문서에서도 딱히 알지 못해도 리액트를 사용하는 데는 문제가 없다고 하지만 저희의 목표는 리액트적 사고를 하는 거였으니 한 번 알아보도록 해요 ^^,,

 

컴포넌트 내부에서 useState를 여러 번 호출하면 리액트는 각 호출이 서로 다른 변수를 가리키고 있다는 걸 알아야 해요.

아래 코드처럼 index와 showMore라는 두 개의 state 변수가 있을 때, 리액트는 useState(0)이 index에 해당하고, useState(false)가 showMore에 해당한다는 걸 알아야 합니다.

const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);

 

이런 경우 리액트는 어떻게 useState(0)이 index이고, useState(false)는 showMore인지 알아낼까요?

특별한 변수 이름이나 식별자를 쓰지 않기 때문에 헷갈릴 것 같지만! 리액트는 특별한 방식으로 이 문제를 해결합니다.

호출 순서를 기억해요

리액트는 훅의 호출 순서가 항상 같다는 점을 이용해서 이를 해결해요. useState가 컴포넌트의 최상위에서만 호출되도록 규칙을 지키면 리액트는 렌더링할 때마다 훅들이 항상 같은 순서로 호출된다고 믿을 수 있습니다. 그래서 각 useState 호출이 어떤 state를 가리키는지 정확하게 기억할 수 있게 되는 거예요.

index라는 state를 생성하는 useState가 항상 첫 번째 위치에 있고, showMore라는 state를 생성하는 useState가 항상 두 번째 위치에 있기 때문에 리액트는 각 호출이 어느 state와 연결되어 있는지 헷갈리지 않아요!

내부적으로 리액트는 어떻게 관리할까?

리액트는 각 컴포넌트에 대해 state 배열을 가지고 있어요. 이 배열은 컴포넌트의 여러 state를 저장하는 공간이에요.

렌더링할 때마다 리액트는 이 배열을 통해 어떤 state가 어디에 있는지 기억하고 useState를 호출할 때마다 다음 state를 가져와서 반환하는 방식으로 작동합니다.

요약하자면!

✔️ 리액트는 훅 호출 순서를 믿고 각 state를 관리해요.

✔️ useState를 호출할 때마다 다음 state를 꺼내서 반환합니다.

✔️ 이 방법 덕분에 별도의 식별자 없이도 각 state가 올바른 위치에 할당되는 것!

 

이렇게 리액트는 매 렌더링 시 정확한 순서로 훅이 호출된다는 전제 아래 모든 state를 관리할 수 있게 되는 거예요!

State는 격리되고 비공개로 유지된다?

리액트에서 state는 컴포넌트마다 고유하게 관리됩니다. 만약 똑같은 컴포넌트를 여러 번 화면에 표시하더라도 각 컴포넌트의 state는 서로 독립적으로 작동해요.

 

똑같은 Gallery 컴포넌트를 두 개 화면에 표시하고 각 Gallery 안에 "Next" 버튼이 있다고 해볼게요. 첫 번째 Gallery에서 "Next" 버튼을 눌러도 두 번째 Gallery는 그와 상관없이 자기 상태를 그대로 유지하는 걸 볼 수 있어요. 하나의 컴포넌트를 변경해도 다른 컴포넌트의 state는 전혀 영향을 받지 않는다는 걸 알 수 있습니다!

 

https://codesandbox.io/p/sandbox/5rx42k

 

https://codesandbox.io/p/sandbox/5rx42k

 

codesandbox.io

 

왜 이렇게 설계 되었을까?

이런 구조 덕분에 컴포넌트의 state가 지역적으로 격리될 수 있어요.

쉽게 말하자면 컴포넌트가 자기 내부의 데이터(ex,, 이미지 갤러리의 현재 페이지)를 독립적으로 관리할 수 있습니다.

이렇게 하면 복잡한 UI에서도 데이터가 엉키지 않고 각 컴포넌트가 고유한 상태를 유지할 수 있어요!

일반 변수와의 차이점?

일반 변수를 사용해도 컴포넌트 간에 데이터가 공유될 수 있는데, state는 컴포넌트에 "속한" 정보이기 때문에 서로의 영향을 받지 않도록 리액트가 따리 관리해줘요.

그래서 state는 코드 내 위치나 함수 호출과 연결되지 않고 화면의 특정 위치에 딱 붙어있는 정보로 생각할 수 있어요!

부모 컴포넌트는 자식 컴포넌트의 state를 '알지 못 한다'

리액트의 state는 props와 달리 부모 컴포넌트가 알 수 없고, 변경할 수도 없어요.

Page라는 부모 컴포넌트는 각 Gallery 컴포넌트가 어떤 state를 가지고 있는지 전혀 알 수 없습니다.

state는 선언된 컴포넌트 내에서만 유효한 비공개 데이터이기 때문이에요.

 

이런 구조 덕분에 컴포넌트 간의 상태 변화가 독립적으로 유지될 수 있고 나중에 어떤 컴포넌트에 state를 추가하거나 제거하더라도 다른 컴포넌트에는 영향을 주지 않게 됩니다.


두 컴포넌트가 같은 state를 공유하려면?

근데... 만약 두 개의 Gallery가 동일한 state를 공유해야 한다면 어떻게 해야 할까요?!

이 경우에는 state를 각 Gallery 내부에 두기보다는 두 Gallery의 가장 가까운 공통 부모 컴포넌트로 state를 끌어올려야 해요.

그렇게 하면 부모 컴포넌트에서 state를 관리하고 두 개의 자식 Gallery에 props를 통해 state를 전달할 수 있어요.

 

이걸 리액트에서는 상태 끌어올리기라고 한다고 합니다! (이건 추후에 또 다루더라구요,,, 그때 더 자세히 알아보는 걸로 ^_^)


 

저는 항상 개발할 때마다 상태 관리 설계에 대해 많은 고민을 하게 되는데요 😭

전역 상태 관리 라이브러리를 쓰기도 하지만 이런 방식들이 복잡해질수록 코드 관리가 어려워지더라구요...

혹시 여러분은 컴포넌트 독립성을 유지하면서도 상태를 쉽게 공유하기 위해 어떤 방법을 사용하시나요?