Back to list

#06. State와 useState

목차9

TL;DR

  • State = “한 컴포넌트의 가변적인 기억(Mutable memory of a single component)”
  • UI = f(props, state)
    • 더 단순하게, UI = State(What) + Logic(How)
  • State 선언 방식
    • const [값, set값] = useState(초기값)
  • State는 변수 재할당이 아니라 Setter로 바꿈
  • Rerender 조건 = Setter로 호출 + 값이 실제로 바뀜

개요

React 뿐 아니라 선언형 UI를 추구하는 라이브러리/프레임워크들은 항상 UI를 얘기할 때 State를 다룬다.
이 글에서는 그 State가 정확히 뭔지, React에서는 어떻게 사용되는지 살펴보자.


State란 무엇인가

일전에, React는 모든 UI를 함수로 바라본다고 했다.

UI = f(props, state)

잠깐 UI를 구성하는 요소를 뜯어보자.

  • UI가 그릴 정보들
    • name, email, age같은 데이터
  • UI의 현재 상태
    • 단순히 ‘보인다/안 보인다’가 아니라, ‘초기 상태’, ‘강조됨’, ‘데이터 로딩 중’, ‘로딩 실패(에러)’ 등
  • UI Logic
    • 상태에 따라 UI를 어떻게 그릴지
    • 로딩 중이면 로딩 아이콘을 띄우고;
    • 데이터를 잘 받아왔으면 정해진 레이아웃으로 보여주고;
    • 실패하면 ‘재시도’ 버튼을 보여줌

위 수식에 대입하면, 그릴 정보가 props, 현재 상태가 state, 둘에 따라 어떻게 그릴지가 함수 f다.

개인적으로는, 나는 아래 정의가 더 명확하고 받아들이기 편한 것 같다:

UI = State + Logic

여기서 State는 “그릴 데이터 + UI의 상태”, 즉 What이고, Logic은 그 상태를 어떻게 그릴지, 즉 How다.
(실제로 코드를 치다 보면 ‘상태’와 ‘데이터’가 칼같이 나뉘지는 않는다)

조금 더 엄밀한 정의

React 공식적으로 State를 정의하자면 “Mutable memory of a single component”이다.
특징 둘만 짚으면:

  • 가변성(Mutability)
    • UI의 상태는 시간이 지나며 바뀔 수 있다.
  • Rerendering
    • State가 업데이트되면, React가 알아서 변경된 상태를 UI에 반영한다.

useState로 State 선언하기

요약하자면, State는 “이 값이 바뀌면 화면에 반영되어야 할 때” 사용한다.
사용법은 간단하다 — useState를 쓰면 된다.

const [userName, setUserName] = useState("");
const [postTitles, setPostTitles] = useState(["Post1", "Post2", "Post3"]);
  • userName, postTitles: State의 이름(현재 값)
  • setUserName, setPostTitles: State를 바꾸는 함수(Setter).

여기서 Setter의 이름을 뭘로 할지는 자유지만, 관례적으로는 set{이름}을 쓴다.

let이 아니라 const일까

“State는 바뀌니까 let이겠지”라고 생각할 수도 있는데, 관례는 const다.
그리고 여기엔 이유가 있다.

State는 변수를 재할당해서 바뀌는 게 아니다.

  1. Setter가 호출됨
  2. React가 Rerender 시작
  3. 함수가 처음부터 다시 실행됨
  4. 업데이트 된 값이 담긴 새 State 가 만들어짐”

즉, 매 렌더에서 userName은 그 렌더 동안에는 고정된 스냅샷이므로, 의미상으로도 const가 합리적으로 보인다.

State는 Setter로만 바꾼다

State를 다룰 때 중요한 점 중 하나는, State 업데이트를 위해서는 반드시 함께 받은 Setter로 바꿔야 한다.
이는 Setter가 단순히 값을 바꿀 뿐 아니라, React에게 State 바뀌었음을 알려주기 때문이다.

const [title, setTitle] = useState("초기값입니다");

return (
  <div>
    <h4>{title}</h4>
    <button onClick={() => setTitle("바보")}>!! PRESS ME !!</button>
  </div>
);

여기서 onClick={() => setTitle("바보")}를 눈여겨보자.
화살표 함수로 감쌌다이전 글의 1-A 참고.

그런데 만약, onClick={ setTitle("바보") }처럼 쓰면 어떻게 될까?
여기서는 React의 “Render-phase update”에 대한 사전 이해가 필요하다:

초기 렌더 도중 Setter가 호출될 경우, React는 “낡은 화면이니까 처음부터 다시 그려야겠네”로 받아들임

즉, 아래의 플로우를 거친다:

  1. 화면 렌더 시작
  2. onClick을 만나는 순간, Setter가 즉시 호출된다.
  3. Setter 호출 = 낡은 화면? → 처음부터 다시
  4. 화면 렌더 시작
  5. onClick을 만나는 순간, Setter가 즉시 호출된다.
  6. React는 이를 ‘낡은 화면’으로 인식하고, 다시 처음부터 렌더 시작
  7. 작작 렌더링하라고 에러 뱉고 죽음

React가 일반적으로 허용하는 초기 Re-render 한계는 25회이다.
이를 넘어서면 “Too many re-renders. React limits the number of renders to prevent an infinite loop.”가 발생하고, 이러면 화면이 안그려진다.

그럼 직접 바꾸면?

title을 Setter 없이 직접 건드리면 어떻게 될까?

  • 변수 재할당
    • 예를들어 title = "새 값"
    • const로 선언했다면 에러가 발생한다. (관례에서 state를 const로 선언하는 이유 중 하나)
    • 만약 let로 선언되었다면, 에러는 발생하지 않지만 화면에 반영되지 않는다.
    • 다른 놈이 Rerender를 유발할 경우, 변경된 값도 유실된다
      • React가 해당 State가 업데이트 되었는지 알 수단이 없으므로.
    • 다만 아래같은 케이스는 조금 다름:
let [abc, setAbc] = useState(5);

return(
  <div>
    <p>{abc}</p>  // 5 렌더
    { abc = 22; }
    <p>{abc}</p>  // 22 렌더
  </div>
)  // 5, 22가 나타남
  • 내부 변형
    • 예를들어, 배열/객체 State에 arr.push(...) 같은 in-place mutation
    • 에러는 안나지만 Rerender도 발생하지 않음
    • 심지어 다른 놈이 Rerender를 트리거할 경우 변경된 값이 유실됨
    • 여기 참고.

즉 어느 쪽이든, Setter를 거치지 않으면 UI 반영도 없고 오히려 State와 화면이 괴리되는 문제만 발생할 수 있다.


결론

계속 몇몇 내용이 반복되는 것 같은데, 그만큼 스스로의 철학과 원리를 잘 준수한다는 의미같다.
생각하다보면 결국 ‘왜’에 도달하고 → 여기에는 보통 근본적인 이유가 있음.

링크가 복사되었습니다