상태가 필요한 이유
React 컴포넌트에서 데이터를 정의하는 기본적인 방법 : let, const 키워드 활용
ㄴ 변수에 저장된 값이 변경되더라도 화면에는 반영되지 않는다는 단점
React는 상태 기반의 UI 프레임워크
--> 특정 값이 변경되었을 때 화면을 자동으로 업데이트 하려면 '상태'라고 불리는 개념으로 관리 필요
상태를 관리하는 방법
- useState hook
- useReducer hook
useState - 1
상태(state) : 시간이 지남에 따라 변할 수 있는 데이터
리액트 훅(hook) : 함수형 컴포넌트에서 상태 관리와 생명 주기 기능 및 부가적인 기능을 활용할 수 있게 도와주는 새로운 기능
useState 훅의 기본적인 문법 형태
const [state, setState] = useState<Type>(initialState)
- state
- 실제 상태값이 할당될 변수
- == 상태 변수
- 값이 변경되면 화면을 다시 그리는 리렌더링을 통해 즉시 반영
- setState
- 상태값을 변경할 때 사용하는 함수
- == 상태 업데이트 함수
- Type
- 상태값의 타입
- 생략하는 경우가 많지만, 초기값과 변경될 데이터가 다를 경우에는 명시해주어야 함
- initialState
- 상태의 초기값
useState 훅은 React 패키지에서 import 해야 사용 가능
useState는 두 개의 값을 반환
- 실제 할당될 상태값
- 해당 상태값을 업데이트할 수 있는 함수
// App.tsx
import { useState } from "react";
export default function App() {
const [state, setState] = useState<number>(0);
const handleSetToTen = () => {
setState(10);
}
return (
<>
<h1>state: {state}</h1>
<button onClick={handleSetToTen}>Set To 10</button>
</>
);
}
useState - 2
초기 상태값과 변경된 후의 상태값의 타입이 다를 경우에는 제네릭 타입 생략 불가능
// App.tsx
import { useState } from "react";
export default function App() {
const [state, setState] = useState<number | string>(0); // 초기에는 number, 나중엔 string
const handleSetToTen = () => {
setState("10"); // string 타입
}
return (
<>
<h1>state: {state}</h1>
<button onClick={handleSetToTen}>Set To 10</button>
</>
);
}
상태 업데이트 방법
상태 변수에 할당된 값은 반드시 상태 업데이트 함수를 통해서만 변경
--> React는 상태값이 변경 것을 추적
--> 변경된 상태를 기반으로 리렌더링
렌더링 원리
- 배치 업데이트
- 성능 최적화를 위해, 여러 개의 상태 업데이트를 하나의 리렌더링으로 묶어서 처리하는 방식
- 클로저와 스냅샷
- React가 렌더링을 할 때, 그 시점의 상태 변수 값은 초기값으로 고정
상태 업데이트 방식
- 직접 업데이트 방식
- 상태 업데이트 함수에 변경할 값을 직접 넣어주는 방식
- setState(value);
- ex) setCount(count+1);
- 함수형 업데이트 방식
- 상태 업데이트 함수의 인수로 함수를 작성하는 방식
- setState((state) => state);
- ex) setCount((prev) => prev+1);
직접 업데이트 방식
// App.tsx
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const handleIncrement = () => {
setCount(count + 1); // 0 + 1
setCount(count + 1); // 0 + 1
setCount(count + 1); // 0 + 1
}
return (
<>
<h1>Count: {count}</h1>
<button onClick={handleIncrement}>increment</button>
</>
);
}
^ 버튼을 클릭할 때마다 count가 1씩 증가 (O) / 3씩 증가 (X)
ㄴ 이러한 결과가 나타나는 이유는 배치 업데이트와 클로저 두 가지 원인이 복합적으로 얽혀있기 때문
배치 업데이트 - setCount()를 세 번 호출했지만, 한 번 호출될 때마다 한 번의 리렌더링을 발생시키는 것이 아님
클로저 - count의 초기값이 0이기 때문에 실질적으로 각각의 setCount(count + 1)이 (0 + 1)을 의미
함수형 업데이트 방식
// App.tsx
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const handleIncrement = () => {
setCount((count) => count + 1); // 0 + 1
setCount((count) => count + 1); // 1 + 1
setCount((count) => count + 1); // 2 + 1
}
return (
<>
<h1>Count: {count}</h1>
<button onClick={handleIncrement}>increment</button>
</>
);
}
^ 버튼을 클릭할 때마다 count가 3씩 증가 (O) / 1씩 증가 (X)
함수형 업데이트 방식에서는 항상 함수의 매개변수에 최신의 상태값이 들어옴
ㄴ 배치 업데이트에 의해서 한 번의 렌더링에 모든 상태 변경을 몰아서 처리한다고 하더라도, 클로저에 구애받지 않음
상태 업데이트 방법 복습하기
상태 업데이트 방식 선택 기준 : 변경될 값이 현재 상태 값을 참조하느냐, 참조하지 않느냐로 결정
객체 상태 정의하기
객체 데이터로 정의된 상태값 중 일부만 업데이트하고 싶을 때에는 스프레드 연산자 활용
// App.tsx
import { useState } from "react";
export default function App() {
const [userInfo, setUserInfo] = useState({
name: "jack",
age: 20,
gender: "male",
});
const handleUpdateUserInfo = () => {
setUserInfo({
...userInfo, // 스프레드 연산자
name: "mike" // 바꿀 값으로 덮어씌우기
});
};
return (
<>
<p>name: {userInfo.name}</p>
<p>age: {userInfo.age}</p>
<p>gender: {userInfo.gender}</p>
<button onClick={handleUpdateUserInfo}>Update User Info</button>
</>
);
}
객체 상태 정의하기 심화
여러 개의 객체가 중첩되어 있는 경우에도, 스프레드 연산을 활용해 기존 객체를 전개하고 덮어 씌우는 방식으로 코드를 작성
// App.tsx
import { useState } from "react";
export default function App() {
const [userInfo, setUserInfo] = useState({
name: "jack",
age: 20,
gender: "male",
contact: {
email: "jack@example.com",
phone: "123-456-7890",
},
address: {
home: {
street: "123 Main St",
city: "New York",
zipCode: "10001",
},
office: {
street: "456 Business Ave",
city: "New York",
zipCode: "10002",
},
},
});
const handleUpdateUserInfo = () => {
setUserInfo({
...userInfo,
name: "mike",
age: 30,
contact: {
...userInfo.contact,
email: "react@naver.com"
},
address: {
home: {
...userInfo.address.home,
street: "321 Main st",
},
office: {
...userInfo.address.office,
street: "134 Ave",
}
}
});
};
return (
<>
<pre>{JSON.stringify(userInfo, null, 2)}</pre>
<button onClick={handleUpdateUserInfo}>UpdateUserInfo</button>
</>
);
}
배열 상태 다루기
배열 요소 추가하기
// App.tsx
import { useState } from "react";
export default function App() {
const [fruits, setFruits] = useState(['apple', 'banana', 'orange']);
const handAddFruit = () => {
setFruits((fruits) => [...fruits, "melon"]); // 함수형 업데이트 방식
};
return (
<>
<p>{fruits.join(", ")}</p>
<button onClick={handAddFruit}>Add Fruit</button>
</>
);
}
배열 요소 변경하기
// App.tsx
import { useState } from "react";
export default function App() {
const [fruits, setFruits] = useState(['apple', 'banana', 'orange']);
const handAddFruit = () => {
setFruits((fruits) => fruits.map(fruit => fruit === 'apple' ? 'grape' : fruit)); // 함수형 업데이트 방식
};
return (
<>
<p>{fruits.join(", ")}</p>
<button onClick={handAddFruit}>Add Fruit</button>
</>
);
}
배열 요소 사이에 새로운 요소 추가하기
// App.tsx
import { useState } from "react";
export default function App() {
const [fruits, setFruits] = useState(['apple', 'banana', 'orange']);
const handAddFruit = () => {
setFruits((fruits) => [...fruits.slice(0, 1), 'grape', ...fruits.slice(1)]); // 함수형 업데이트 방식
};
return (
<>
<p>{fruits.join(", ")}</p>
<button onClick={handAddFruit}>Add Fruit</button>
</>
);
}
상태 독립성 이해하기
useState로 생성한 '상태'는 각각의 컴포넌트에서 독립적
// App.tsx
import FirstCount from "./components/FirstCount";
import SecondCount from "./components/SecondCount";
export default function App() {
return (
<>
<FirstCount />
<SecondCount />
</>
);
}
// FirstCount.tsx
import { useState } from "react";
export default function FirstCount () {
const [count, setCount] = useState(0);
return (
<>
<h1>FirstCount Component: {count}</h1>
<button onClick={() => setCount((count) => count+1)}>increment</button>
</>
);
}
// SecondCount.tsx
import { useState } from "react";
export default function SecondCount () {
const [count, setCount] = useState(0);
return (
<>
<h1>SecondCount Component: {count}</h1>
<button onClick={() => setCount((count) => count+1)}>increment</button>
</>
);
}
* 컴포넌트 내부에서 정의되어져 있는 상태값은 해당 컴포넌트 내부에서만 유효
상태 끌어올리기
독립적으로 상태가 변경되고 있는 두 컴포넌트에서, 상태를 하나로 통합하여 공유하는 방법
: 두 컴포넌트의 공통적인 부모 또는 조상 컴포넌트에서 상태를 정의하고 해당 상태와 상태 업데이트 함수를 props로 전달
// App.tsx
import { useState } from "react";
import FirstCount from "./components/FirstCount";
import SecondCount from "./components/SecondCount";
export default function App() {
// 부모 컴포넌트 내부에서 정의
const [count, setCount] = useState(0);
return (
<>
{/* props로 전달 */}
<FirstCount count={count} setCount={setCount} />
<SecondCount count={count} setCount={setCount} />
</>
);
}
// FirstCount.tsx
import { Dispatch, SetStateAction } from "react";
export default function FirstCount({
count,
setCount,
}: {
count: number;
setCount: Dispatch<SetStateAction<number>>;
}) {
return (
<>
<h1>FirstCount Component: {count}</h1>
<button onClick={() => setCount((count) => count + 1)}>increment</button>
</>
);
}
// SecondCount.tsx
import { Dispatch, SetStateAction } from "react";
export default function SecondCount({
count,
setCount,
}: {
count: number;
setCount: Dispatch<SetStateAction<number>>;
}) {
return (
<>
<h1>SecondCount Component: {count}</h1>
<button onClick={() => setCount((count) => count + 1)}>increment</button>
</>
);
}
^ 여러 자식 컴포넌트가 동일한 상태를 공유하고, 일관된 방식으로 상태를 업데이트하는 것이 가능
캡슐화 이해하기
캡슐화 : 특정 함수 내부에 상태 업데이트 로직을 구현하는 것
상태 업데이트 함수를 직접 넘겨주는 것보다는, 캡슐화한(상태 업데이트 로직을 구현한) 함수를 넘겨주는 것을 권장
// App.tsx
import { useState } from "react";
import FirstCount from "./components/FirstCount";
import SecondCount from "./components/SecondCount";
export default function App() {
const [count, setCount] = useState(0);
// 캡슐화 - 미리 선언
const handleIncrement = () => {
setCount((count) => count + 1);
}
return (
<>
{/* props로 전달 */}
<FirstCount count={count} handleIncrement={handleIncrement} />
<SecondCount count={count} handleIncrement={handleIncrement} />
</>
);
}
// FirstCount.tsx
export default function FirstCount({
count,
handleIncrement,
}: {
count: number;
handleIncrement: () => void;
}) {
return (
<>
<h1>FirstCount Component: {count}</h1>
<button onClick={handleIncrement}>increment</button>
</>
);
}
// SecondCount.tsx
export default function SecondCount({
count,
handleIncrement,
}: {
count: number;
handleIncrement: () => void;
}) {
return (
<>
<h1>SecondCount Component: {count}</h1>
<button onClick={handleIncrement}>increment</button>
</>
);
}
리액트 훅 공통 규칙
- 리액트 훅은 use로 시작해야 함
- 리액트에서 사용하는 모든 훅은 use로 시작
- useState도 리액트 훅의 한 종류이기 때문에 use로 시작
- 리액트에서 사용하는 모든 훅은 use로 시작
- 훅은 최상위에서만 호출되어야 함
- 훅은 컴포넌트 함수의 최상위에서만 호출
- 조건문이나 반복문 내에서 호출 불가능
useReducer - 1
useReducer : 컴포넌트를 정의하는 또다른 방법
ㄴ useState 훅에 비해 복잡한 상태 관리를 하는 데 효율적
useReducer 훅의 기본적인 문법 형태
const [state, dispatch] = useReducer<Type>(reducer, initialState)
- state
- 실제 상태값이 할당될 변수
- == 상태 변수
- dispatch
- reducer 함수를 호출해서 새로운 상태값 설정
- == 액션 발생 함수
- Type
- 상태값의 타입
- 보통 초기값의 타입을 적어줌
- reducer
- 리듀서 함수
- useReducer에서 현재 상태 값을 결정하는 역할
- initialState
- 상태의 초기값
useReducer - 2
reducer 함수 구조
function reducer(state, action) { ... }
- state : 현재의 상태값
- action : 어떻게 바꿀지에 대한 정보가 담긴 객체
- type, payload 두 가지 정보를 담고 있음
- type : 어떤 행동을 할지 나타내는 '이름표' (주로 대문자로 작성)
- 리턴값 : 새로운 상태값을 계산해서 리턴
reducer 함수는 reducer 폴더 아래에 별도의 파일(.ts)로 빼기도 함
// App.tsx
import { useReducer } from "react";
import counterReducer from "./reducer/counterReducer";
export default function App() {
// const [state, dispatch] = useReducer(reducer, 0);
const [count, countDispatch] = useReducer(counterReducer, 0);
return (
<>
<h1>Count: {count}</h1>
<button onClick={() => countDispatch({ type: "DECREMENT" })}>감소</button>
<button onClick={() => countDispatch({ type: "RESET" })}>리셋</button>
<button onClick={() => countDispatch({ type: "INCREMENT" })}>증가</button>
</>
);
}
// counterReducer.ts
export default function counterReducer(
count: number,
action: { type: string } // type 속성이 string인 객체
) {
switch (action.type) {
case "INCREMENT":
return count + 1;
case "DECREMENT":
return count - 1;
case "RESET":
return 0;
default:
return count;
}
}
'React > 타입스크립트로 배우는 리액트(React.js) : 기초부터 최신 기술까지' 카테고리의 다른 글
| 섹션 8. 컴포넌트 스타일링 (2) (0) | 2026.04.21 |
|---|---|
| 섹션 8. 컴포넌트 스타일링 (1) (0) | 2026.04.16 |
| 섹션 7. 반복 렌더링과 조건부 렌더링 (0) | 2026.04.09 |
| 섹션 5. 컴포넌트와 이벤트 (0) | 2026.03.27 |
| 섹션 4. 컴포넌트와 Props (0) | 2026.03.26 |