Front-end

왜 정규 변수가 아닌 useState를 사용해야 할까?

FuterNomad 2023. 2. 18. 11:02

최근 회사에서 React를 통해 프로젝트를 진행하면서, "한 컴포넌트 내에서 useState를 많이 사용해도 괜찮을까?" 에 대한 고민을 하게 되었습니다. 그리고 이 고민과 더불어

  • useState를 적게 사용하기 위해서 component를 더 작은 단위로 나눠야 하는 것일까?
  • useState가 아닌 일반 정규 변수(let, const)를 통해 event가 발생했을 때 update하는 식으로 useState를 대체할 수 있지 않을까?

와 같은 어떤 방식이 렌더링 최적화를 이끌어낼 수 있을까에 대해 알고 싶어졌습니다. 이런 고민들을 하다 보니 제가 useState를 React에서 왜 사용하는지를 제대로 이해하지 않고 사용해서 생겨나는 문제라는 생각이 들었고, 이 글을 작성하게 되었습니다.


글의 구성은 아래와 같이 진행됩니다.

  1. React에서 Hooks를 사용하게 된 계기
  2. state란 무엇인가?
  3. 🗝️ useState를 왜 사용하는가?

 


 

1. React에서 Hooks를 사용하게 된 계기

먼저, useState는 React에서 제공하는 Built-in React Hooks 중 하나입니다.
Hooks는 개발자가 만든 컴포넌트로부터 나온 다른 React 기능들을 사용할 수 있게 해줍니다.

Hooks가 생겨난 Motivation을 공식문서를 통해 살펴보면,
Hooks는 Classes를 이용해 Component를 다루며 생기는 여러 문제를 해결하기 위해 나타났습니다.

문제 1. 컴포넌트 사이에서 상태기반(stateful) 로직을 사용하기 어렵다.

V16.8 이하 환경에서는 컴포넌트 재사용을 가능하게 하는 행동을 연겨하는 "attach"기능을 제공하지 않았기 때문에 이를 해결하기 위해 개발자들이 render props와 higher-order components 패턴을 사용함으로써 컴포넌트를 사용할 때 크고 무거운 양의 코드를 재구성하게 하는 문제를 야기했습니다. 따라서 React는 상태기반 로직을 공유하기 위한 더 나은 로직이 필요했습니다.

따라서, Hooks를 통해 상태기반 로직을 컴포넌트로부터 확장할 수 있고, 독립적으로 테스트가 가능하며, 재사용할 수 있게 하였습니다. Hooks는 컴포넌트 계층 재배치 없이 상태유지 로직을 재사용 가능하게 해줍니다.

 

문제 2. 복잡한 컴포넌트는 이해하기 어려워진다.

React를 사용하여 프로젝트를 진행하다 보면 간단하게 시작한 컴포넌트에서 관리 불가능할 정도로 지저분해진 상태 유지 로직과 사이드 이펙트를 유지보수 해야 하는 경우가 자주 생깁니다. 또한, 각 생명주기 method와 관련 없는 로직도 자주 포함하고 있습니다. 같이 변화하는 관계된 코드는 서로 분할되지만, 완전히 관계없는 코드의 경우에는 한 method로 결합됩니다. 이는 bug나 불일치를 쉽게 야기합니다.

많은 사례에서 상태유지 로직에 모든 곳에 존재해야 하기 때문에 이 컴포넌트들을 작은 단위로 쪼개는 것은 불가능합니다. 그리고 이 점이 많은 사람들이 React와 별도의 상태 관리 라이브러리를 결합하는 이유 중에 하나입니다. 그러나 이는 너무 많은 추상화를 초래하고, 다른 파일들 사이를 오가야 하며, 컴포넌트 재사용을 어렵게 만듭니다.

이를 해결하기 위해, 생명주기 method 기반 분할을 강요하기 보다 Hooks는 한 컴포넌트를 관련된 조각들(구독 설정 또는 data fetching)로 이루어진 작은 함수들로 분할하게 합니다. 또한 컴포넌트의 지역 상태를 reducer를 사용하여 더 예측가능하게 만들도록 관리하는 것을 선택할 수 있습니다.

 

문제 3. Classes는 사람과 기계 모두 혼란스럽게 만든다.

Class는 코드 재사용과 코드 조직을 더 어렵게 만드는 데다가, React를 배우는 데 큰 장벽이 될 수 있습니다. 다른 대다수 언어들과 동작하는 방식이 매우 다른 this용법에 대해 this가 JavaScript에서 동작하는 방식을 이해해야 합니다. 또한, event handler를 bind하는 것을 기억하고 있어야 합니다. ES2022 public class fields 없이는 코드가 매우 장황해집니다. 사람들은 props, state, top-down, flow를 완벽하게 이해하고 있지만, classes에서는 고전합니다. React에서 함수형 컴포넌트와 클래스형 컴포넌트를 각각 언제 사용해야 할지에 대해서는 개발자들 사이에서 의견 차이가 발생합니다.

더욱이, 컴포넌트의 ahead-of-time compliation은 더 많은 잠재력을 가지고 있습니다.
(*ahead-of-time compliation: 실행 시간에 운영하는 데 필요한 일의 양을 줄이기 위해서 높은 수준의 프로그래밍 언어를 실행하기 전 빌드하는 시간에 주로 저수준의 언어로 컴파일링 하는 행위)
최근, React에서는 Prepack을 사용하여 컴포넌트 folding을 실험 하였고, 이를 통해 유망한 결과를 보았습니다.
(*Prepack: JavaScript 코드를 더 빠르게 실행시켜주는 도구)
(*Component Folding: 컴파일러 최적화; 기본적으로 React가 어떻게 동작하는 지에 대한 정보를 이용하여 확실한 조건 하에서 컴포넌트 folding과 inlining을 지속하는 아이디어입니다.)
그러나 클래스형 컴포넌트에서는 이러한 최적화 fallback을 느린 경로로 만들 수 있는 의도하지 않은 패턴을 조장할 수 있다는 것을 발견했습니다. Classes는 오늘날 도구의 issue를 대표합니다. 예를 들어, classes는 최소화를 잘 하지 못하고, 재로딩을 불안정하게 하며, 신뢰할 수 없게 만듭니다. React는 코드 최적화 경로에 머무를 수 있는 API를 대표하고 싶어합니다.

앞선 문제들을 해결하기 위해 Hooks는 더 많은 React의 기능들을 classes 없이 사용하게 합니다. 개념적으로 React는 함수친화적입니다. Hooks는 함수를 포함하지만, 실용적인 React의 정신을 희생하지 않습니다. Hooks는 명령형 탈출 창구 (imperative escape hatches)에 대한 접근을 제공하고, 복잡한 함수형 또는 반응형 프로그래밍을 학습할 필요가 없습니다.


정리해보자면, 컴포넌트 간 상태 유지 로직 공유, 컴포넌트 분리, Classes기반에서 함수 친화형으로의 변화를 위해 React에서는 Hooks를 사용하게 되었습니다.


 

2. state란 무엇인가?

State: A Component's Memory

컴포넌트들은 종종 상호작용의 결과로 화면에 있는 것들을 바꿔야 합니다. form에 값을 입력하는 것은 input field를 업데이트 해야 하고, :"다음" 버튼을 클릭하면 img carousel은 보여져야 할 이미지를 바꿔야 하며, "구매" 버튼을 클릭하면 상품이 쇼핑 카트에 담겨야 합니다. 컴포넌트는 현재 input에 입력된 값, 현재 이미지, 쇼핑 카트에 대한 정보를 기억해야 합니다. React에서는 이러한 컴포넌트 특정 기억해야 할 정보들을 state라고 합니다.

When a regular variable isn’t enough(정규 변수가 충분하지 않은 경우)

이 질문이 제가 이 글을 포스팅 하게 된 계기입니다. 마침 React Docs에 이 질문에 대한 설명이 나와있었습니다.

아래 "next"버튼을 클릭하면 index+1을 실하게 하는 코드가 적혀있습니다. 그러나 이를 실행하면 화면에서 index가 변화하지 않는 것을 확인할 수 있습니다.

export default function App() {
  let index = 0;

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

  return (
    <>
      <button onClick={handleClick}>
        Next
      </button>
      <h3>  
        {index + 1}
      </h3>
    </>
  );
}

위 코드에서 handleClick event handler는 지역 변수인 index를 업데이트 합니다. 그러나 두 가지 요인이 화면에 변화가 보여지는 것을 막습니다.

  1. 지역 변수는 렌더링 사이에서 값이 유지되지 않습니다. React가 컴포넌트를 두번째로 렌더링 했을 때, 처음부터 렌더링됩니다. 따라서 지역 변수에 어떠한 변화도 고려하지 않습니다.
  2. 지역 변수의 변화는 렌더링을 일으키지 않습니다. React는 지역 변수에 새로운 값에 의해 렌더링을 해야 할 필요를 깨닫지 못합니다.

새로운 값으로 컴포넌트를 업데이트 하기 위해서는 두 가지 일이 발생되어야 합니다.

  1. 렌더링 사이에서 값 유지하기
  2. React가 새로운 값으로 재렌더링 하도록 촉발하기

 

3. useState를 왜 사용하는가?

앞서 지역 변수를 사용 했을 때, 화면에 변화가 나타나지 않는 이유와 화면에 변화를 나타내기 위해 필요한 요인들에 대해 살펴보았습니다.
이전에 설명했던 새로운 값으로 컴포넌트를 업데이트 해야하는 이유를 통해 useState를 사용하는 이유에 대해서 알아보면,

useState Hook은 두 가지를 제공합니다:

  1. 렌더링 사이에서 값을 유지하는 state 변수
  2. 변수를 업데이트 하고, React가 컴포넌트를 재렌더링 하도록 촉발시키는 state setter function

 

useState 구조
const [index, setIndex] = useState[0]

const onClick = () => setIndex(index+1)

return (
	<button onClick={onClick}>plus</button>
)

위 코드를 작성함으로써 useState를 호출하게 되면,

  1. 컴포넌트는 첫번째 렌더링을 하게 됩니다. 그 이유는 useState에 초기값으로 0을 넘겼고, 이는 [0, setIndex]를 return 합니다. 그리고 React는 0을 가장 최신 state value로 기억합니다.
  2. state를 업데이트 하는 경우, 예를 들어 사용자가 "plust"버튼을 클릭함으로써 setIndex(index+1)이 발생하면, index는 0 이었기 때문에 setIndex(1)이 됩니다. 이는 React에 index를 지금부터 1로 기억해야 하고, 재렌더링을 일으켜야 한다고 전달합니다.
  3. 따라서 두번째 렌더링이 일어납니다. React는 여전히 useState(0) Hook을 바라보지만, index가 1임을 setIndex 함수를 통해 전달했기 때문에 [1, setIndex]를 return합니다.
  4. 이러한 과정이 계속해서 이루어지게 됩니다.

 

컴포넌트에 다중 변수 설정하기

우리는 한 컴포넌트에서 여러 useState를 가질 수 있습니다.
서로 관련이 없는 state에 대해서는 다중 state 변수를 가지는 것이 좋은 방법입니다. 그러나, state 변수가 함께 변화하는 경우가 잦다면, 하나의 변수로 결합하는 것이 더 나은 방법일 수 있습니다. 예를 들어 많은 입력 값을 받는 form의 경우가 입력값마다 state를 두는 것보다 하나의 state를 통해 관리하는 것이 더 적합합니다.

State is isolated and private

만약 같은 컴포넌트를 두 번 렌더링하게 되면, 각 복사본은 따로 완전히 고립된 상태가 됩니다. 따라서 컴포넌트 중 하나를 다른 컴포넌트에 영향을 끼치지 못하도록 바꿔야 합니다. State는 특정 함수 호출이나 코드에 한 부분에 종속되어 있지 않고, 화면에 있는 특정 부분에 "local"(지역 한정)되어 있습니다. 따라서, 같은 컴포넌트를 두 개 호출해도, 각 state는 따로 저장됩니다. props와 달리 state는 선언되어진 컴포넌트 안에서 완전히 private합니다. 부모 컴포넌트도 자식 컴포넌트에 state를 바꿀 수 없습니다. 이는 다른 컴포넌트들에 끼칠 영향과 관계 없이 state를 추가하고, 삭제할 수 있다는 것을 의미합니다.

만약 같은 컴포넌트들의 state를 동시에 유지하고 싶다면, 자식 컴포넌트에 있는 state를 제거하고, 가장 가까운 부모 컴포넌트에 state를 추가하는 방법이 적합합니다.

 


 

공식 문서를 통해 정규변수를 state처럼 사용하려고 하면 렌더링이 trigger되지 않는 다는 것을 알았고,
useState는 호출된 컴포넌트에 private하게 종속되기 때문에 컴포넌트 내에서만 렌더링이 필요한 경우에는 컴포넌트 내에 선언하여 사용하고, 여러 컴포넌트에서 동시에 일어나야 하는 state의 변화가 필요한 경우에는 부모 컴포넌트에서 props로 전달하는 방식을 채택해 사용해야 한다는 것을 알게되었습니다. 또한, state 변화는 렌더링을 일으키기 때문에 렌더링 최적화를 위해서는 관련이 있는 state의 경우 single source로 관리하는 것이 더 효율적이라는 것도 깨달았습니다.

 


References

https://beta.reactjs.org/learn/state-a-components-memory

'Front-end' 카테고리의 다른 글

Next.js 13 <Link /> with Middleware  (0) 2023.09.12
Next.js 13 Paradigm  (0) 2023.09.05
zustand(with TypeScript) | 사이드 프로젝트에서 사용해보기  (0) 2023.01.17
🐻zustand 이해하기  (0) 2023.01.16