React의 대표 hooks - useState
리액트 함수 컴포넌트에서 가장 중요한 개념은 바로 훅입니다.
훅은 클래스 컴포넌트에서만 가능했던 state, ref등의 리액트의 핵심적인 기능을 함수에서도 가능하게 만들었고,
무엇보다 클래스 컴포넌트보다 간결하게 작성할 수 있어 훅이 등장한 이후로 대부분의 React 코드는 함수 컴포넌트로 작성되고 있습니다.
이러한 hook중 가장 대표적인 useState에 대해서 상세하게 알아보려합니다.
1. useState는 Closure를 통해 상태관리를 한다.
React의 가장 기본적인 훅은 당연 useState 일것입니다.
useState는 함수 컴포넌트 내부에서 상태를 정의하고, 이 상태를 관리할 수 있게 해주는 훅입니다.
만약 useState를 사용하지 않고 일반 변수를 사용해서 사용해서 상태값을 관리한다고 가정해봅시다.
const Component = () => {
let state = 0
const handleButtonClick = () => {
state = state + 1
}
return (
<>
<h1>{state}</h1>
<button onClick={handleButtonClick}>hi</button>
</>
)
}
handleClick event handler는 지역변수 state를 업데이트 하고있습니다. 하지만 이러한 변화는 보이지 않는데 그 이유는
1. 지역 변수는 렌더링 간에 유지되지 않습니다.
: React는 re-rendering시 지역 변수에 대한 변경 사항은 고려하지 않고 처음부터 렌더링 합니다.
2. 지역 변수를 변경해도 렌더링을 일으키지 않습니다.
: React의 re-rendering 조건에 변수는 포함되지 않습니다.
[참고]
import React from 'react'
const Component = () => {
const [,triggerRender] = useState()
let state = 'hello'
function handleButtonClick() {
state = 'hi'
triggerRender()
}
return (
<>
<h1>{state}</h1>
<button onClick={handleButtonClick}>hi</button>
</>
)
}
변로수는 리렌더링이 일어나지 않으므로, useState의 두번째 반환값을 실행해 렌더링이 일어나게끔 코드를 작성해보았습니다.
하지만 이 코드도 작동하지 않는데요. 그 이유는 리엑트는 매번 렌더링이 발생할 때마다 함수가 다시 새롭게 실행되는데,
실행되는 함수에서 state는 매번 'hello'로 초기화 되기 때문에 값의 변화가 반영되지 않기때문입니다.
그렇다면 어떻게해야 리렌더링이 일어날때 state의 값을 유지할 수 있을까요?
실제 useState는 아니지만 useState의 기본적인 작동을 구현한 코드입니다.
function useState(initialValue) {
// _val은 useState에 의해 만들어진 지역 변수입니다.
var _val = initialValue
function state() {
// state는 내부 함수이자 클로저입니다.
// state()는 부모 함수에 정의된 _val을 참조합니다.
return _val
}
function setState(newVal) {
// _val를 노출하지 않고 _val를 변경합니다.
_val = newVal
}
return [state, setState]
}
var [foo, setFoo] = useState(0)
// useState의 스코프 내부에 있는 _val를 변경합니다.
setFoo(1)
console.log(foo()). // 1출력
이 함수에는 state와 setState라는 두 개의 내부 함수가 있습니다. state는 상단에 정의된 지역 변수 _val를 반환하고, setState는 전달 된 매개 변수 (예: newVal)로 지역 변수를 설정합니다.
중요한 것은 foo와 setFoo를 사용하여 내부 변수 _val에 접근하고 덮어쓰기를 할 수 있다는 것입니다.
이 둘은 useState의 스코프에 대한 접근 권한을 가지고 있고, 이러한 참조를 클로저라고 합니다.
이렇듯 useState는 자바스크립트의 특징 중 하나인 클로저 Closure를 사용함으로써 해당 값을 노출시키지 않고 오직 리액트에서만 쓸 수 있었고, 함수 컴포넌트가 매번 실행되더라도 useState에서 이전의 값을 정확하게 꺼내 쓸 수 있게 되었습니다.
그렇다면 본격적으로 useState를 사용해 보겠습니다. 우선 useState의 기본 사용법입니다.
import {useState} from 'react'
const [state, setState] = useState(initialState)
useState의 인수로는 사용할 state의 초기값을 넘겨줍니다. 아무런 값을 넘겨주지 않으면 초기값은 undefined입니다.
배열 구조 분해를 사용하여 [state, setState] 과 같은 state 변수의 이름을 지정하는 것이 관례입니다.
컴포넌트가 렌더링될 때마다, useState는 다음 두 개의 값을 포함하는 배열을 제공합니다.
- 저장한 값을 가진 state 변수 (index).
- state 변수를 업데이트하고 React에 컴포넌트를 다시 렌더링하도록 유발하는 state setter 함수 (setIndex).
실제 동작방식은 아래와 같습니다. (버튼은 클릭하면 index가 1씩 증가하는 컴포넌트)
const [index, setIndex] = useState(0);
- 컴포넌트가 처음 렌더링 됩니다.
index의 초깃값으로 useState를 사용해 0을 전달했으므로 [0, setIndex]를 반환합니다. React는 0을 최신 state 값으로 기억합니다. - state를 업데이트합니다.
사용자가 버튼을 클릭하면 setIndex(index + 1)를 호출합니다. index는 0이므로 setIndex(1)입니다. 이는 React에 index는 1임을 기억하게 하고 또 다른 렌더링을 유발합니다. - 컴포넌트가 두 번째로 렌더링 됩니다.
React는 여전히 useState(0)를 보지만, index를 1로 설정한 것을 기억하고 있기 때문에, 이번에는 [1, setIndex]를 반환합니다. - 이런 식으로 계속됩니다!
이러한 useState는 하나의 컴포넌트에 원하는 만큼 많은 타입의 state변수를 가질 수 있고,
각 컴포넌트의 인스턴스에 지역적 이게 됩니다.
즉, 동일한 컴포넌트를 여러개 렌더링 한다고 하더라도 각 복사본은 완전히 격리된 state를 가지게 됩니다.
import Button from './button'
const Page = () => {
return (
<>
<Button />
<Button />
</>
)
}
// Button.jsx
import React, { useState } from 'react'
const Button = () => {
const [index, setIndex] = useState(0)
function handleButtonClick() {
setIndex(index + 1)
}
return (
<>
<h1>{index}</h1>
<button onClick={handleButtonClick}>Button</button>
</>
)
}
export default Button
Page 컴포넌트에서 Button 컴포넌트를 두번 렌더링 했지만, 그 2개의 state는 서로 독립적이게 작동합니다.
이것이 state를 일반적인 모듈 상단에 선언할 수 있는 보통의 변수와 구별할 수 있는 요소입니다.
State는 특정 함수 호출이나 코드 내의 특정 위치와 관련이 없고, 화면의 특정 위치에 '지역적' 입니다.
또한, Page 컴포넌트는 Button 컴포넌트의 state에 대해 아무것도 알지 않고, 심지어 그것이 있는지도 모릅니다.
Props와 달리 state는 선언한 컴포넌트에 완전히 비공개 입니다.
2. 각 렌더링의 state값은 고정되어있다.
State 변수는 읽고 쓸 수 있는 일반 자바스크립트 변수처럼 보일 수 있습니다.
하지만 state는 snapshot처럼 동작합니다.
state 변수를 설정하여도 이미 가지고 있는 state 변수는 변경되지 않고, 대신 re-rendering이 요청됩니다.
Rendering이란?
React가 컴포넌트, 즉 함수를 호출한다는 뜻입니다. 해당 함수에서 반환하는 JSX는 시간상 UI의 스냅 샷과 같습니다.
prop, 이벤트 핸들러, 로컬 변수는 모두 렌더링 당시의 state를 사용해 계산됩니다.
React가 컴포넌트를 re-rendering할 때는 아래와 같이 동작합니다.
1. React가 함수를 다시 호출합니다.
2. 함수가 새로운 JSX 스냅샷을 반환합니다.
3. 그러면 React가 함수가 반환한 스냅샷과 일치하도록 화면을(DOM tree) 업데이트합니다.
🔻 상세 예시
age가 42라고 가정합니다. 이 핸들러는 setAge(age + 1)를 세 번 호출합니다.
function handleClick() {
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
}
age는 45가 될 것이라고 예상할 수 있지만, 43이 됩니다.
왜냐하면 React는 렌더링의 이벤트 핸들러 내에서 state 값을 '고정'으로 유지하기 때문입니다.
setAge를 호출해도 이미 실행 중인 코드에서 age state 변수는업데이트가 되지않습니다.
또한 React는 event handler가 실행을 전부 마친 후 state를 업데이트 처리 합니다.
이 때문에 re-rendering은 모든 setAge() 호출이 완료된 이후에만 일어납니다.
(음식점 주문받는 웨이터가 첫번째 요리를 말하자마자 주방으로 달려가지않고, 주문이 끝날때까지 기다리는 상황)
이렇게 하면 너무 많은 re-rendering이 발생하지 않고도 여러 컴포넌트에서 나온 다수의 state 변수를 업데이트 할 수 있습니다.
이는 event handler와 그 안에 있는 코드가 완료될 때까지 UI가 업데이트되지 않는다는 의미 입니다.
batching이라고하는 이 동작은 React앱을 훨씬 빠르게 실행할 수 있게 도와주고, 일부 변수만 업데이트된 반쯤 완성된 혼란스러운 렌더링을 처리하지 않게 해줍니다.
각각의 setter가 핸들러 안에서 작동하도록 하려면 setAge에 업데이터 함수를 전달하여 해결합니다.
function handleClick() {
setAge(a => a + 1); // setAge(42 => 43)
setAge(a => a + 1); // setAge(43 => 44)
setAge(a => a + 1); // setAge(44 => 45)
}
여기서 a => a + 1은 업데이터 함수입니다. 이 함수는 대기 중인 state를 가져와서 다음 state를 계산합니다.
React는 업데이터 함수를 큐에 넣습니다. 그러면 다음 렌더링 중에 동일한 순서로 호출합니다.
- a => a + 1은 대기 중인 state로 42를 받고 다음 state로 43을 반환합니다.
- a => a + 1은 대기 중인 state로 43을 받고 다음 state로 44를 반환합니다.
- a => a + 1은 대기 중인 state로 44를 받고 다음 state로 45를 반환합니다.
대기 중인 다른 업데이트가 없으므로, React는 결국 45를 현재 state로 저장합니다.
여기서 알아야할 것은 다음 업데이터 함수에 전달되는 것은 업데이트된 age state값이 아니라
이전의 업데이트 함수에서 return한 값이 다음 업데이트 함수로 전달된다는 것입니다.
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
setNumber(42);
}}>
이 이벤트 핸들러가 React에 지시하는 작업은 다음과 같습니다.
- setNumber(number + 5) : number는 0이므로 setNumber(0 + 5)입니다.
> React는 “5로 바꾸기” 를 queue에 추가합니다. - setNumber(n => n + 1) : n => n + 1는 업데이터 함수입니다.
> React는 해당 함수를 queue에 추가합니다.
다음 렌더링하는 동안 React는 state queue를 순회합니다.
queued update | n | returns |
(0+5=5) 5로 바꾸기 | 0 (unused) | 5 |
n => n+1 | 5 | 5+1 = 6 |
React는 6을 최종 결과로 저장하고 useState에서 반환합니다.
'Study > React' 카테고리의 다른 글
React 깊이 이해하기(3) - React Rendering (1) | 2024.11.08 |
---|---|
React 깊이 이해하기(2) - Life cycle (0) | 2024.11.07 |
React 깊이 이해하기(1) - 가상 DOM (Virtual DOM), Fiber tree (0) | 2024.10.25 |