#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는 변수를 재할당해서 바뀌는 게 아니다.
- Setter가 호출됨
- React가 Rerender 시작
- 함수가 처음부터 다시 실행됨
- 업데이트 된 값이 담긴 새 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는 “낡은 화면이니까 처음부터 다시 그려야겠네”로 받아들임
즉, 아래의 플로우를 거친다:
- 화면 렌더 시작
onClick을 만나는 순간, Setter가 즉시 호출된다.- Setter 호출 = 낡은 화면? → 처음부터 다시
- 화면 렌더 시작
onClick을 만나는 순간, Setter가 즉시 호출된다.
…- React는 이를 ‘낡은 화면’으로 인식하고, 다시 처음부터 렌더 시작
- 작작 렌더링하라고 에러 뱉고 죽음
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를 트리거할 경우 변경된 값이 유실됨
- 여기 참고.
- 예를들어, 배열/객체 State에
즉 어느 쪽이든, Setter를 거치지 않으면 UI 반영도 없고 오히려 State와 화면이 괴리되는 문제만 발생할 수 있다.
결론
계속 몇몇 내용이 반복되는 것 같은데, 그만큼 스스로의 철학과 원리를 잘 준수한다는 의미같다.
생각하다보면 결국 ‘왜’에 도달하고 → 여기에는 보통 근본적인 이유가 있음.
