useState
useState
란 무엇인가?useState
는 React 훅 중 하나로, 함수형 컴포넌트에서 상태를 관리할 수 있게 해준다. 함수형 컴포넌트는 단순히 입력을 받아 출력을 반환하는 함수로, 상태를 갖지 않는다. 하지만 useState
를 사용하면 함수형 컴포넌트에도 상태를 추가할 수 있다.
함수형 컴포넌트는 React에서 UI를 정의하는 가장 기본적인 컴포넌트 유형 중 하나이다. 이름에서 알 수 있듯이, 이 컴포넌트는 자바스크립트 함수로 정의되며, 단순히 props(컴포넌트에 전달된 입력 데이터)를 받아 JSX(자바스크립트 확장 문법)를 반환한다. 이 JSX는 React가 화면에 그릴 요소들을 정의하는 데 사용된다.
function Greeting(props) {
return <h1>Hello, {props.name}!</h1>;
}
위의 예제에서 Greeting
은 함수형 컴포넌트이다. 이 컴포넌트는 name
이라는 props를 받아서 "Hello, {name}!"이라는 문장을 반환한다. React는 이 반환된 JSX를 해석하여 DOM 요소로 변환하고, 화면에 렌더링한다.
함수형 컴포넌트의 가장 큰 특징은 순수 함수(pure function)처럼 동작한다는 것이다. 즉, 주어진 입력(props)에 따라 항상 동일한 출력을 반환하며, 컴포넌트 자체에 내부 상태나 부수 효과가 없다는 가정이 포함된다. 이 때문에 함수형 컴포넌트는 초기에는 상태를 가질 수 없었다. 상태를 관리하거나, 컴포넌트 생명 주기(lifecycle)에 따라 특정 작업을 수행하려면 클래스형 컴포넌트를 사용해야 했다.
하지만 함수형 컴포넌트가 상태를 가질 수 없다는 제약은 훅(Hooks)이 도입되면서 변경되었다. 이제 useState
, useEffect
와 같은 훅을 사용하여 함수형 컴포넌트에서도 상태를 관리하고, 부수 효과를 처리할 수 있게 되었다.
useState
사용법useState
는 리액트의 핵심 기능으로, 다음과 같은 형식으로 사용된다.
const [state, setState] = useState(initialValue);
state
: 현재 상태의 값이다. 예를 들어, 버튼 클릭 수를 나타내는 상태라면, state
는 현재 클릭된 횟수를 담고 있다.setState
: 상태를 업데이트하는 함수이다. 이 함수를 호출하면 상태가 변경되고, React가 UI를 다시 렌더링한다.initialValue
: 상태의 초기값이다. 예를 들어, 카운터를 0에서 시작하고 싶다면 useState(0)
과 같이 초기값을 설정할 수 있다.useState
를 사용하는가?useState
를 사용하면 컴포넌트의 상태를 관리하고, 상태가 바뀔 때마다 React가 자동으로 UI를 업데이트해 주기 때문에 매우 편리하다. 복잡한 상태 관리나 UI 업데이트 로직을 직접 작성할 필요가 없으며, 상태 관리가 쉬워져 애플리케이션을 더 직관적이고 예측 가능하게 만든다.
useState
의 동작 원리와 내부 메커니즘useState
의 동작 원리useState
는 컴포넌트가 렌더링될 때, 리액트의 내부 메모리에 상태를 저장한다. 컴포넌트가 처음 렌더링될 때, useState
는 초기 상태 값을 받아서 내부적으로 관리하고, 이 상태 값은 컴포넌트가 다시 렌더링되더라도 유지된다.
상태를 업데이트하는 setState
함수를 호출하면, 리액트는 해당 컴포넌트를 다시 렌더링하여 화면을 업데이트한다. 이때, 새로운 상태 값이 적용된 컴포넌트가 렌더링되어 최신 상태를 반영한다. 리액트는 가상 DOM을 사용해 변경된 부분만 효율적으로 업데이트하기 때문에 성능상 이점이 크다.
useState
의 내부에는 JavaScript의 클로저 개념이 적용되어 있어, 함수형 컴포넌트가 매번 호출될 때마다 상태가 유지되고, React의 가상 DOM과 결합하여 상태가 변경될 때마다 효율적으로 UI를 업데이트할 수 있다. 클로저의 개념을 이해하면 useState
의 동작 방식을 더 깊이 이해할 수 있으며, 함수형 컴포넌트에서의 상태 관리가 어떻게 이루어지는지 명확히 알 수 있다.
useState
의 관계useState
의 동작 원리를 더 깊이 이해하려면 JavaScript의 중요한 개념 중 하나인 클로저(Closure)를 알아야 한다. 클로저는 함수가 생성될 때 함수 내부에서 참조하는 변수를 함수가 생성된 환경에서 "기억"하는 특성을 말한다.
클로저는 함수가 선언될 때의 환경(렉시컬 환경)을 "기억"하고, 이 환경에 접근할 수 있는 함수이다. 클로저를 사용하면 함수가 생성될 때의 변수 상태를 유지할 수 있다.
function outer() {
let outerVar = 1;
function inner() {
console.log(outerVar);
}
return inner;
}
const closure = outer();
closure(); // 출력: 1
위 예제에서 outer
함수는 outerVar
라는 변수를 선언하고, inner
라는 함수를 반환한다. outer
함수가 종료되면 outerVar
는 사라지는 것처럼 보이지만, closure
함수가 outerVar
를 참조하고 있기 때문에 outerVar
는 여전히 유지된다. 이처럼 inner
함수는 자신이 선언될 때의 환경을 기억하는 클로저이다.
useState
와 클로저useState
도 클로저의 개념을 활용하여 동작한다. useState
는 함수형 컴포넌트가 매번 호출될 때마다 초기값을 다시 설정하지 않고, 기존의 상태를 유지할 수 있게 한다. 이는 useState
가 함수형 컴포넌트의 렌더링 사이에서 상태를 "기억"하기 때문이다.
예를 들어, 다음 코드를 보자.
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Increase</button>
</div>
);
}
여기서 useState(0)
은 처음 렌더링될 때 0을 초기값으로 설정한다. 이후 setCount
를 호출하면, 컴포넌트가 다시 렌더링되더라도 count
는 setCount
에 의해 업데이트된 값을 유지한다. 이는 useState
가 클로저를 이용해 상태를 기억하기 때문이다. useState
가 반환하는 setCount
함수는 내부적으로 상태값을 "기억"하고 있는 클로저로, 상태가 변경될 때마다 이 클로저를 통해 상태를 업데이트하고 유지한다.
클로저 덕분에 useState
는 함수형 컴포넌트에서 상태를 관리할 수 있게 된다. 상태가 함수의 매개변수처럼 계속 전달되는 것이 아니라, 클로저를 통해 함수 내부에서 지속적으로 유지되기 때문에, 함수형 컴포넌트는 매번 새로운 상태를 가지는 것이 아니라 이전의 상태를 이어받을 수 있다. 이러한 클로저의 특성은 React가 컴포넌트를 효율적으로 관리하고, 상태를 일관되게 유지하는 데 중요한 역할을 한다.
useState
의 내부 메커니즘useState
훅은 React의 함수형 컴포넌트에서 상태를 관리할 수 있게 해준다. React는 컴포넌트가 매번 리렌더링될 때마다 그 컴포넌트를 다시 호출하기 때문에, 상태를 유지하기 위해서는 특정 메커니즘이 필요하다. 이 메커니즘의 핵심은 "훅 호출 순서"이다.
React는 컴포넌트가 처음 렌더링될 때 useState
가 호출된 순서를 기억해둔다. 이 순서에 따라 상태를 저장하고 관리하게 된다. 이후 컴포넌트가 리렌더링될 때 React는 이전에 저장된 상태를 같은 순서로 반환한다. 이렇게 하면 함수형 컴포넌트가 매번 새롭게 호출되더라도 상태가 유지될 수 있다.
예를 들어, 다음과 같은 컴포넌트를 보자.
function ExampleComponent() {
const [count, setCount] = useState(0); // 첫 번째 useState
const [name, setName] = useState('Alice'); // 두 번째 useState
// JSX 반환
}
이 컴포넌트에서 useState(0)
은 count
상태를 관리하고, useState('Alice')
는 name
상태를 관리하게 된다. React는 이 컴포넌트가 처음 렌더링될 때 count
와 name
상태를 해당 순서대로 저장한다. 그리고 컴포넌트가 다시 렌더링될 때도 동일한 순서로 useState
를 호출하여, 같은 순서로 상태를 반환하게 된다.
useState
의 호출 순서는 매우 중요하다. 이 순서가 변경되면 React는 잘못된 상태를 반환하게 된다. 예를 들어, 조건문 안에서 useState
를 호출하는 경우가 있다면, 이는 React가 상태를 관리하는 데 큰 혼란을 초래할 수 있다.
잘못된 사용 예를 보자.
function ConditionalComponent() {
const [count, setCount] = useState(0);
if (count > 0) {
const [name, setName] = useState('Alice'); // 조건에 따라 호출되는 useState
}
// JSX 반환
}
여기서 count
가 0일 때는 name
상태가 생성되지 않지만, count
가 0보다 커지면 name
상태가 생성된다. 이 경우, useState
의 호출 순서가 달라지게 되며, React는 이전 렌더링과 다른 순서로 상태를 처리하게 된다. 결과적으로 잘못된 상태 값이 UI에 반영되거나, 상태가 제대로 업데이트되지 않는 문제가 발생할 수 있다.
React는 컴포넌트가 렌더링될 때, 컴포넌트 내부에서 호출된 모든 useState
훅을 "리스트"처럼 순서대로 저장한다. 이때 React는 각 useState
호출을 "인덱스"라는 번호로 구분하여 관리한다.
function MyComponent() {
const [count, setCount] = useState(0); // 인덱스 0
const [name, setName] = useState('Alice'); // 인덱스 1
// JSX 반환
}
이 예시에서 React는 두 개의 useState
를 인덱스 0과 1에 각각 할당한다.
useState(0)
이 호출됨 → React는 이를 인덱스 0에 저장하고, count
상태를 관리.useState('Alice')
이 호출됨 → React는 이를 인덱스 1에 저장하고, name
상태를 관리.count
상태를 꺼내어 반환하고, 인덱스 1에서 name
상태를 꺼내어 반환한다. function MyComponent() {
const [count, setCount] = useState(0); // 인덱스 0
if (count > 0) {
const [name, setName] = useState('Alice'); // 조건에 따라 달라지는 인덱스
}
// JSX 반환
}
이 예시에서는 useState
가 조건문 안에서 호출된다. 이로 인해, count
값에 따라 useState
의 호출 순서가 달라진다.
count
가 0일 때)useState(0)
이 호출됨 → React는 이를 인덱스 0에 저장하고, count
상태를 관리.count > 0
)이 false이므로, useState('Alice')
는 호출되지 않음 → 따라서 인덱스 1은 사용되지 않음.count
가 1이 된 후)useState(0)
이 다시 호출됨 → React는 인덱스 0에서 count
상태를 꺼내어 반환.useState('Alice')
가 호출됨 → React는 이를 인덱스 1에 새롭게 할당하려 함.여기서 문제가 발생한다.
useState
호출 순서가 변했다고 착각하고, 상태가 엉뚱한 위치에 저장될 수 있다.useState
가 추가되면, 이전 상태와 새로운 상태가 뒤섞여, 잘못된 상태 관리가 일어날 수 있다.인덱스 0, 1, 2가 있는 배열이 있고, 각각의 인덱스에 useState
로 설정된 상태가 저장되어 있다. 첫 번째 렌더링에서는 [0, 1, 2]
와 같이 저장되었다. 그런데 두 번째 렌더링에서 1
이 없어지고 [0, 2]
가 된다면, 이제 React는 어디에 새 값을 저장할지 헷갈리게 된다.
결국, 상태가 꼬이게 되고, React는 이전에 저장된 상태를 올바르게 불러오지 못하게 된다. 이로 인해, 컴포넌트가 의도한 대로 동작하지 않게 된다.
그래서 React에서 useState
는 항상 같은 순서로 호출되어야 한다. 그래야 React가 각 상태를 정확한 인덱스에 맞춰 관리할 수 있다.
React는 각 컴포넌트가 처음 렌더링될 때 훅이 호출된 순서대로 상태를 기억한다. 이 순서 기반의 관리 방식 덕분에, 컴포넌트가 여러 번 리렌더링되더라도 상태가 일관되게 유지된다. 하지만, 이는 동시에 훅이 항상 같은 순서로 호출되어야 한다는 제약을 의미한다. 훅 호출 순서가 달라지면 React는 잘못된 상태를 반환할 수 있으므로, 훅을 조건문이나 반복문 안에서 사용하는 것을 피해야 한다.
결론적으로, useState
를 포함한 모든 훅은 항상 동일한 순서로 호출되어야 하며, 이것이 함수형 컴포넌트에서 상태 관리가 안정적으로 이루어지게 하는 핵심 원리이다.
useState
의 활용과 유용한 패턴다음은 useState
를 사용한 간단한 카운터 예시이다.
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
여기서 useState(0)
은 초기값으로 0을 가지는 상태를 설정한다. 사용자가 버튼을 클릭할 때마다 setCount
가 호출되어 count
상태가 증가하고, React는 자동으로 화면을 업데이트해 클릭 횟수를 반영한다.
때로는 상태가 단순한 값이 아니라, 여러 개의 값을 포함하는 객체일 수 있다. 이 경우 useState
를 사용해 객체 상태를 관리할 수 있다.
function Form() {
const [form, setForm] = useState({ name: '', email: '' });
const handleChange = (e) => {
setForm({
...form,
[e.target.name]: e.target.value
});
};
return (
<div>
<input name="name" value={form.name} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
<p>{form.name}</p>
<p>{form.email}</p>
</div>
);
}
이 예시에서는 객체 상태(form
)를 관리하고 있으며, 사용자가 입력 필드를 변경할 때마다 해당 필드의 값이 상태에 반영된다. 상태를 객체로 관리할 때는 기존 객체를 복사하고, 변경된 부분만 업데이트하는 것이 중요하다. 이는 불변성(immutability)을 유지하기 위함인데, 객체의 불변성을 유지하면 리액트가 상태 변경을 쉽게 감지하고 효율적으로 업데이트할 수 있다.
useState
초기값을 함수로 설정하기useState
는 초기값을 함수로 설정할 수도 있다. 이렇게 하면 초기 상태를 계산하는 작업이 비용이 많이 드는 경우에 유용하다. 초기값 설정 함수는 컴포넌트가 처음 렌더링될 때만 호출된다.
function ExpensiveComponent() {
const [value, setValue] = useState(() => {
return expensiveCalculation();
});
return <div>{value}</div>;
}
이 예시에서 expensiveCalculation()
함수는 초기값을 계산하는 데 사용되며, 이 계산은 컴포넌트가 처음 렌더링될 때 한 번만 수행된다.
useState
를 사용할 때 고려해야 할 사항useState
는 간단한 상태를 관리하기에는 좋지만, 상태가 복잡해지면 여러 개의 useState
를 사용해야 할 수 있다. 이 경우 코드가 복잡해지고 가독성이 떨어질 수 있다. 복잡한 상태 관리가 필요할 때는 useReducer
같은 다른 훅을 사용하는 것이 더 적절할 수 있다. useReducer
는 복잡한 상태 로직을 간단하게 만들 수 있는 방법을 제공한다.
useState
의 상태 업데이트 함수인 setState
는 비동기적으로 동작한다. 즉, 상태가 즉시 업데이트되지 않고, 리액트가 다음 렌더링에서 상태를 업데이트한다. 따라서 상태가 업데이트된 직후에 그 값을 참조하려면 주의해야 한다.
function Example() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count); // 이전 상태 값이 출력될 수 있음
};
return (
<button onClick={handleClick}>Click me</button>
);
}
위 예시에서 console.log(count)
는 예상과 달리 업데이트되기 전의 상태를 출력할 수 있다. 이는 setState
가 비동기적으로 동작하기 때문이다.
리액트는 컴포넌트 상태 관리 외에도 전체 애플리케이션 상태를 관리할 수 있는 방법을 제공한다. 예를 들어, useContext
를 사용해 여러 컴포넌트 간에 상태를 공유할 수 있다. 대규모 애플리케이션에서는 Redux와 같은 상태 관리 라이브러리를 사용하는 것이 더 적절할 수 있다.