useState
는 리액트에서 특정 값의 상태를 접근하고 제어할 수 있게 해주는 Hook
입니다. 일반적으로 값을 가감하거나 초기화 하기 위한 비동기적 업데이트를 위해 사용되며 가장 보편적으로 사용되는 Hook
인데요.
해당 Hook
을 사용하기 위해서는 React의 모듈로부터 임포트를 받은 후 해당 Hook
을 호출하여 해당 값의 초기 상태를 설정하고, 관리 할 값(변수)과 함수명(해당 값을 관리할)에 각각 구조 분해 할당합니다.
import {useState} from react const [state, setState] = useState(0) // 해당 값의 초기 상태는 0이고 state는 현재의 상태 값을 나타내는 변수를, setState는 그 값에 상태를 업데이트 하는 메서드 이름입니다.
만약 리액트에서 제공하는 useState가 아니라 그냥 함수로 직접 상태를 변경한다면 어떻게 될까요?
let anotherCounter = 0 function anotherButton() { anotherCounter += 1; } <button className="button" onClick={anotherButton}>{anotherCounter}</button>
위의 예제는 전역 변수 anotherCounter
를 핸들링하는 anotherButton
함수를 이용해 해당 변수의 상태를 업데이트 하는 예제인데요.
하지만 아쉽게도 훅스에서 관리하는 변수는 해당 코드문을 이용해 사용자가 리액트에서 를 업데이트 하는 방법으로 리액트 컴파일러는 해당 변수의 변화를 감지하지 못합니다..
그 이유는 리액트에서는 상태의 불변성을 유지하면서 변화에 따라 Virtual DOM을 이용해 추적하고, 새로운 상태를 생성된 뒤에 해당 상태를 반영하는 방식을 차용하고 있기 때문인거죠.
한마디로 리액트 라이브러리인 훅스에서 관리하는 변수는 해당 변수를 제어하는 메서드로만 제어가 가능하다는 뜻입니다.
물론 forceUpdate
메서드를 이용해 상태를 강제적으로 변경하는 방법도 존재하나, 가급적이면 리액트에서는 이 Hook
을 이용해 상태를 제어하는 방법이 기본이라고 할 수 있는 것이죠.
만약 이러한 useState
를 자바스크립트로 구현해 보면 다음과 같이 구현해 볼 수 있을 것입니다.
function useState(initialValue) { let state = initialValue; function setState(Func) { state = Func(state); } return [state, setState]; } const [state, setState] = useState(0); function addState() { setState(state => state + 1); } function subState() { setState(state => state - 1); }
이처럼 useState
를 순수한 자바스크립트 형태로 구현 한다면 이와 같은 구조(완벽한 구조는 아니지만)라고 할 수 있는데요.
이러한 useState
를 사용하기 위해서는 선언된 스테이트의 상태(첫번째 할당)를 어떻게 반환할지 상태 관리 메서드(두번째 할당)에 작성 해주는 인라인 방식도 가능하고요.
특정 상황에서는 상태만 받고, 상태를 관리하는 함수는 받지 않고 쓰는 방식도 활용될 수 있습니다. (이 방식의 경우 커스텀 훅을 받아 사용되는 경우가 주입니다.)
const [counter, setCounter] = useState(0); // 사용자가 버튼을 클릭할 시 counter를 관리하고 있는 setCounter 메서드를 이용해 해당 상태를 1 증가 <button className="button" onClick={() => { setCounter((prev) => prev + 1) cheking();}}>+</button>
다만 이러한 방식은 코드의 가독성을 저해하기에, 가급적 핸들링 함수
(상태 관리 업데이트를 실행하는 통상적인 네이밍의 함수) 안에 해당 메서드를 호출하여 상태를 제어하는 방식을 사용합니다.
const [counter, setCounter] = useState(0); // 해당 메서드를 호출하여 상태를 변경하는 함수를 작성 function handlePlus() { setCounter((prev) => prev + 1); } <button className="button" onClick={handlePlus}>+</button>
이와 같이 핸들링 함수를 이용해 해당 상태 관리 메서드를 호출하는 것이 일반적인데요. 리액트에서는 핸들링 함수 자체에서 사용자가 임의로 지정한 변수나 메서드가 아닌, 리액트에서 제공해주는 useState의 상태 관리 함수로 각각의 상태를 관리하게 된답니다.
좀 더 구체적으로 코드를 살펴보면서 설명을 해보겠습니다. 코드를 살펴보면 선언된 useState
훅스의 변수들을 구조 분해 할당으로 각각 counter
와 setCounter
변수에 할당하는데요.
즉
여기서 counter
는 훅스로 관리하게 될, 변화되는 값을 의미하고 setCounter
는 해당 상태를 관리하는 방법 (메서드) 가 됩니다. 즉 useState
는 일종의 상태와 그 상태를 제어하는 메서드를 반환하는 Hooks의 하나를 의미하는 것이죠.
그리고 괄호 안에 들어가 있는 숫자 (0)
는 해당 스테이트에 저장될 초기 상태를 의미합니다. 위의 코드에서는 counter의 초기값이 0으로 할당되어 초기화 되겠지요.
이때 스테이트를 제어하는 메서드 (setCounter)를 감싸서 이벤트 핸들러로 전달하는 이유는 코드의 재사용, 유지 보수 및 디버깅의 경우 보다 유용한 구간 탐색과 사용이 가능하기에 위의 코드와 같이 전달을 하는 것입니다.
주의할 점은 하나의 훅스는 하나의 상태 및 제어 메서드만을 담당하며, 다수의 상태를 하나의 훅스로 관리하고자 한다면 해당 상태를 초기화 할 때 배열, 혹은 객체의 형태로 할당하여 해당 요소의 특정 상태만 변경시키는 방법을 사용하면 됩니다. (주로 서버에서 받은 값들과 같이배열 안의 다수 객체의 특정 속성을 변경할 때 사용)
import React, { useState } from 'react'; function Counter() { // 여러 상태를 객체로 관리합니다. const [state, setState] = useState({ counter1: 0, counter2: 0 }); // 상태를 변경하는 함수들을 정의합니다. const incrementCounter1 = () => { setState((prevState) => ({ ...prevState, counter1: prevState.counter1 + 1 })); }; const incrementCounter2 = () => { setState((prevState) => ({ ...prevState, // 이전의 상태 값에 counter2: prevState.counter2 + 1 // 변경할 속성 값을 새로 지정합니다. })); }; return ( <div> <p>Counter 1: {state.counter1}</p> <button className="button" onClick={incrementCounter1}>Increment Counter 1</button> <p>Counter 2: {state.counter2}</p> <button className="button" onClick={incrementCounter2}>Increment Counter 2</button> </div> ); } export default Counter;
이 외에도 useState
와 비슷한 방식의 훅스는 뒤에서 설명할 useReducer
가 있겠습니다.
정리하자면 해당 컴포넌트의 상태를 변경 (리렌더링) 하기 위해서는 훅스를 이용해야 하며, 이럴 때 사용하는 보편적인 리액트 라이브러리인 훅스(Hooks)를 사용하게 됩니다.
그런데 위에서 언급한, 리액트에서 useState
를 이용한 상태 업데이트는 비동기적으로 동작합니다. 이는 상태가 변경될 때 리렌더링 사이클 내에서 처리된다는 의미인데요.
그래서 상태를 변경하면 리렌더링이 발생하여 업데이트된 상태가 화면에 반영되지만, 그 직후에 실행되는 코드(console.log와 같은 코드)는 여전히 이전 상태 값을 참조합니다. 그래서 버튼을 클릭한 직후 화면에는 변경된 상태 값이 보이지만, 콘솔에는 이전 상태 값이 출력됩니다.
import { useState } from "react"; function CounterApp() { const [count, setCount] = useState(0); // 증가 버튼 클릭 핸들러 const handleIncrement = () => { setCount(count + 1); console.log("증가 버튼 클릭 : " + count); }; // 감소 버튼 클릭 핸들러 const handleDecrement = () => { setCount(count - 1); console.log("감소 버튼 클릭 : " + count); }; return ( <div style={{ padding: "50px", border: "2px solid black", textAlign: "center", }} <h1>Counter: {count}</h1> <div> <button onClick={handleIncrement} style={{ margin: "10px", padding: "10px" }} Increment </button> <button onClick={handleDecrement} style={{ margin: "10px", padding: "10px" }} Decrement </button> </div> </div> ); } export default CounterApp;
그래서 이러한 문제? 를 해결하려면 useEffect와 같은 상태 관리 Hook을 이용하여 특정 작업을 처리할 수 있답니다.
import { useState, useEffect } from "react"; function CounterApp() { const [count, setCount] = useState(0); // 증가 버튼 클릭 핸들러 const handleIncrement = () => { setCount(count + 1); console.log("증가 : " + count); }; // 감소 버튼 클릭 핸들러 const handleDecrement = () => { console.log("감소 : " + count); }; // count값의 변화가 감지될 때마다 코드 실행 useEffect(() => { console.log("상태 업데이트 후 count 값: ", count); }, [count]); return ( <div style={{ padding: "50px", border: "2px solid black", textAlign: "center", }} <h1>Counter: {count}</h1> <div> <button onClick={handleIncrement} style={{ margin: "10px", padding: "10px" }} Increment </button> <button onClick={handleDecrement} style={{ margin: "10px", padding: "10px" }} Decrement </button> </div> </div> ); } export default CounterApp;
리액트에서는 이러한 상태 업데이트 방식을 스냅샷 이라고 부르는데요. 이러한 스냅샷의 원리는 다음의 코드를 보면서 설명을 해보도록 하겠습니다.
import { useState } from "react"; export default function Counter({ onTotal }) { const [counter, setCounter] = useState(0); console.log('렌더링 Counter: ', counter); const handleCounter = () => { setCounter(counter + 1); setCounter(counter + 1); setCounter(counter + 1); console.log('함수 호출 Counter: ', counter); }; if (onTotal) { onTotal(); } return ( <button onClick={handleCounter}>Counter: {counter}</button> ); }
위 코드를 토대로 카운터의 버튼을 눌렀을 때 handleCounter
가 호출되어 못해도 초기 counter
의 상태(0)에서 setCounter가 세 번 호출 되었으니 여러분들이 기대하시는 리렌더링 이후의 counter의 값은 3일겁니다. JS에서 코드가 스택에 올라가 실행되는 순서와 동일하겠죠? 하지만 실제로는 1이 최종적으로 변화되는 counter의 상태값인데요.
그 이유는 위에서 언급한 스냅샷, 즉 코드가 리렌더링 될 당시의 counter의 상태(0)에서 각각의 코드를 실행하기 때문입니다. 바로 앞전의 콘솔의 출력값이 변화 되기 전의 상태값으로 출력된 이유는 바로 이 때문인데요. 마치 아래의 코드와 같이 말이죠.
const handleCounter = () => { setCounter(0 + 1); setCounter(0 + 1); setCounter(0 + 1); console.log('함수 호출 Counter: ', counter); };
만약 우리가 의도한대로 상태값을 늘리고자 한다면, 상태 업데이트 함수에 일반적인 표현식을 전달하는게 아닌, 아래의 코드와 같이 함수형 업데이트 방식의 콜백 함수를 전달해 주어야 동작을 합니다.
import { useState } from "react"; export default function Counter({ onTotal }) { const [counter, setCounter] = useState(0); console.log('렌더링 Counter: ', counter); const handleCounter = () => { {/* useState의 상태 업데이트 함수에 전달된 콜백 함수의 첫번째 인자는 자동적으로 이전의 상태값이 할당되고, 매개변수 이름은 자율적으로 가능하나 보통은 prev로 지정합니다. */} setCounter((prevCounter) => prevCounter + 1); setCounter((prevCounter) => prevCounter + 1); setCounter((prevCounter) => prevCounter + 1); console.log('함수 호출 Counter: ', counter); };
if (onTotal) {
onTotal();
}
return (
Counter: {counter}
);
}