[React] State와 Hooks(useState, useEffect...)
01
State
State는 컴포넌트의 상태를 나타내며, 변화 가능한 값을 관리하는 데 사용됩니다.
State는 직접 수정할 수 없고, setState를 통해 수정해야 합니다.
// 잘못된 예시
this.state = { name: '박서방' };
// 정상적인 사용법
this.setState({ name: '박서방' });
상태가 변경되면 컴포넌트가 다시 리렌더링 됩니다. 따라서 렌더링과 데이터 흐름에 필요한 값만 state에 포함시켜야 성능 저하를 막을 수 있습니다.
함수형 컴포넌트에서는 React 16.8부터 useState Hook을 사용하여 상태를 관리할 수 있습니다.
Lifecycle (생명주기)
React 컴포넌트는 생성, 업데이트, 제거라는 세 가지 주요 생명주기를 가집니다.
이 과정을 통해 컴포넌트의 동작 방식을 알 수 있습니다.
- 컴포넌트가 생성될 때 필요한 메모리를 할당하고, 컴포넌트가 사라질 때는 소멸자를 통해 메모리를 반환합니다.
- 이는 메모리 누수를 방지하고, 효율적인 성능을 유지하기 위해 필수적인 과정입니다.
라이프 사이클은 메모리 관리를 최적화하는 데 중요한 역할을 합니다.
3가지 생명주기
- 생성될 때 (Mount / 출생)
- 컴포넌트가 처음 DOM에 렌더링 될 때 발생합니다. 이 시점에서 초기화 작업이 수행됩니다. - 업데이트될 때 (Update / 인생)
- 컴포넌트가 화면에 렌더링 된 후, props나 state가 변경되면 업데이트가 발생합니다. (리렌더링 될 때를 의미) - 제거할 때 (UnMount / 사망)
- 컴포넌트가 화면에서 사라지는 순간, 즉 DOM에서 제거될 때 발생합니다. (렌더링에서 제외되는 순간을 의미)
- 이때는 메모리 정리, 타이머 제거, 이벤트 삭제, api호출을 취소 등 필요한 정리 작업을 할 수 있습니다.
라이플사이클 제어
컴포넌트의 라이프 사이클의 단계별로 컴포넌트들이 각각 다른 작업을 수행하도록 만드는 것
Hook
Hook은 함수형 컴포넌트에서 상태와 생명주기 기능을 사용할 수 있게 해주는 함수입니다.
함수형 컴포넌트에서 useState, useEffect 등 다양한 Hook을 사용하여 상태를 관리하거나, 컴포넌트의 생명주기를 처리할 수 있습니다.
* 이름 앞에 use를 붙여 hook이라는 것을 나타내 주어야 합니다.
Hook 사용 규칙
1. Hook은 반드시 함수 컴포넌트이거나 커스텀훅(Custom Hook) 내부에서만 호출이 가능합니다.
2. 조건문이나 반복문 내에서는 사용할 수 없습니다.
// 조건문 내에서 사용하는 경우 (잘못된 예)
function App() {
if(num === 1) {
const [number, setNumber] = useState(0);
}
}
// 반복문 내에서 사용하는 경우 (잘못된 예)
function App() {
while (num < 9) {
const [number, setNumber] = useState(0);
}
}
3. 커스텀훅(Custom Hook)을 직접 만들어 사용가능합니다.
: 함수 키워드 앞에 use만 붙여주면 내부에서 커스텀훅으로 인식합니다.
4. ESLint 플러그인으로 규칙을 강제할 수 있습니다.
// ESLint 설정 파일
{
"plugins": [
"react-hooks"
],
"rules": {
"react-hooks/rules-of-hooks": "error", // Hook 규칙 강제
"react-hooks/exhaustive-deps": "warn" // 의존성 배열 체크
}
}
⭐useState()
useState는 컴포넌트에서 상태를 관리할 수 있도록 도와주는 Hook입니다.
const [state, setState] = useState(initialState);
- initialState: 상태의 초기 값
- state: 현재 상태 값
- setState: 상태 값을 업데이트하는 함수
<예시코드 01>
import React, { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>클릭 횟수 : {count}</p>
<button onClick={() => setCount(count + 1)}> Click! </button>
</div>
);
}
<예시코드 02>
import { useState } from 'react';
import './App.css';
function App() {
// useState를 사용하여 text 상태를 선언하고, 초기값을 빈 문자열로 설정합니다.
const [text, setText] = useState('');
function handleChange(event) {
// input창에 사용자가 입력한 값을 받아오기
// event : onChange
setText(event.target.value);
}
return (
<div className="container">
<input type="text" value={text} onChange={handleChange} />
<p>입력한 텍스트: {text}</p>
</div>
);
}
export default App;
*useState를 사용하여 값이 변경될 때마다 리 렌더링이 발생하나 성능에 큰 영향을 줄 정도는 아니라고 합니다.
" 사용 시 주의사항 "
- 상태 값을 직접 수정하지 말고, 반드시 setState 함수로 상태를 업데이트해야 합니다.
- useState는 컴포넌트 최상단에서만 호출해야 합니다. 조건문, 반복문, 중첩 함수 내에서 호출할 수 없습니다
// 잘못된 예시: 조건문 내에서 사용
function App() {
if (num === 1) {
const [number, setNumber] = useState(0); // X
}
}
// 잘못된 예시: 반복문 내에서 사용
function App() {
while (num < 9) {
const [number, setNumber] = useState(0); // X
}
}
- 클래스형 컴포넌트에서는 사용할 수 없습니다.
- useState를 사용할 때는 반드시 import 문을 통해 호출해야 합니다.
import { useState } from 'react'; // 반드시 중괄호로 감싸서 호출
⭐useEffect()
리액트 컴포넌트의 부수 효과(Side Effect)를 제어하기 위해 사용하는 Hook입니다.
useEffect(() => {
// 부수 효과 코드
}, [dependencies]); // 의존성 배열
- 의존성 배열에 있는 값이 변경될 때만 실행됩니다.
부수 효과(side effects)
부수 효과(side effects)는 부작용이라는 뜻으로 프로그래밍에서는 개발자가 의도치 않은 코드가 실행되면서 버그가 나타나면 사이드 이펙트가 발생했다고 표현합니다.
(리액트에서는 "부수적인 효과" , "파생되는 효과" 정도로 해석 가능합니다.)
부수 효과는 컴포넌트의 렌더링 결과에 영향을 주지 않으면서 수행해야 하는 작업입니다.
예를 들어 데이터 가져오기, 구독 설정, DOM 조작 등이 이에 해당합니다.
<사용 예시>
- 의존성 배열에 값을 넣는 경우: 해당 값이 변경될 때마다 실행됩니다.
useEffect(() => { 이펙트 함수 }, [name]);
- 의존성 배열이 빈 배열인 경우: 컴포넌트가 마운트 될 때와 언마운트될 때 한 번만 실행됩니다.
useEffect(() => { 이펙트 함수 }, []);
- 의존성 배열을 생략하는 경우: 컴포넌트가 업데이트될 때마다 호출됩니다.
useEffect(이펙트 함수);
<예시코드 01>
import React, { useState, useEffect } from 'react';
function App() {
const [count, setCount] = useState(0);
// componentDidMount, componentDidUpdate와 비슷하게 작동합니다.
useEffect(() => {
// 브라우저 API를 사용해서 document의 title을 업데이트 합니다.
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>클릭 횟수 : {count}</p>
<button onClick={() => setCount(count + 1)}>Click!</button>
</div>
);
}
export default App;
<예시코드 02>
import React, { useEffect, useState } from 'react';
function App() {
const [name, setName] = useState('김서방'); // 이름 상태
const [inputValue, setInputValue] = useState(''); // input값을 관리하는 상태
useEffect(() => {
console.log('이름이 변경되었습니다.');
}, [name]); // 의존성 배열에 name 값 : name값이 변경될 때에만 console.log가 실행됩니다.
return (
<div className="container">
<input
className="input"
type="text"
onChange={(e) => setInputValue(e.target.value)}>
</input>
<p className="name">{name}</p>
<button
onClick={() => {
setName(inputValue);
}}
>
이름 변경
</button>
</div>
);
}
export default App;
" 사용 시 주의사항 "
- useEffect는 반드시 컴포넌트 최상단에서 호출해야 하며, 조건문이나 반복문 안에서 사용할 수 없습니다.
// 잘못된 예시
if (num === 1) {
useEffect(() => { /* 수행할 작업 */ }, []);
}
- useEffect는 반드시 import 문을 사용하여 호출해야 합니다.
import { useEffect } from 'react';
cleanup 함수
- useEffect 안에서 return을 사용하면 컴포넌트가 언마운트될 때 정리 작업(cleanup)을 할 수 있습니다.
- 이벤트 리스너나 타이머를 설정했다면, 컴포넌트가 사라질 때 이를 정리(cleanup) 해야 합니다.
useEffect(() => {
// 부수 효과 작업
return () => {
// cleanup 작업 (예: 이벤트 리스너 삭제)
};
}, [dependencies]);
useCallback()
useCallback은 메모이제이션을 통해 함수 재생성을 방지하는 Hook입니다.
매 렌더링마다 새로 생성되는 함수를 특정 조건에서만 재생성하도록 합니다.
const memoizedCallback = useCallback(() => {
// 코드
}, [dependencies]);
메모이제이션(memoization)란?
메모이제이션은 함수 호출 결과를 캐시 하여 동일한 입력값으로 호출될 때 이전에 계산된 결과를 반환하는 기법입니다. 이로 인해 불필요한 함수 호출을 줄이고, 성능 최적화를 할 수 있습니다.
import { useState, useCallback } from 'react';
function Counter() {
const [count, setCount] = useState(0);
// useCallback을 사용하여 메모이제이션된 클릭 핸들러 함수
const handleClick = useCallback(() => {
setCount(count + 1); // 이전 상태를 사용하여 새로운 상태를 설정합니다.
}, [count]); // count가 변경될 때마다 handleClick 함수가 다시 생성됩니다.
return (
<div className="container">
<p>Count: {count}</p>
<button onClick={handleClick}>Increment +</button>
</div>
);
}
export default Counter;
" 사용 시 주의사항 "
- useCallback은 컴포넌트 최상위 레벨에서만 호출할 수 있습니다.
- 반복문이나 조건문 내에서 호출할 수 없습니다.
useMemo()
useMemo는 불필요한 계산을 방지하고 성능을 최적화하는 Hook입니다.
(이름 그대로 Memo를 해두었다가 나중에 다시 쓴다 로 생각하면 좋습니다.)
const memoizedValue = useMemo(() => calculateValue, [dependencies]);
<예시코드 01>
import { useMemo } from 'react';
function TodoList({ todosList, activeTab }) {
const filteredTodos = useMemo(() => filterTodos(todosList, activeTab), [todosList, activeTab]);
return (
<div>
{filteredTodos.map(todo => <div key={todo.id}>{todo.text}</div>)}
</div>
);
}
" 사용 시 주의사항 "
- useMemo는 컴포넌트 최상위 레벨에서만 호출할 수 있습니다.
- Strict Mode에서는 React가 실수로 발생한 오류를 찾기 위해 계산 함수를 두 번 호출합니다.
이는 개발 환경에서만 동작합니다.
useReducer()
useReducer는 useState보다 복잡한 상태를 관리할 때 사용하는 Hook입니다.
주로 상태와 상태를 업데이트하는 함수를 결합하여 상태를 관리합니다.
(모든 useState는 useReducer로 대체 가능 => 상태 관리 코드를 컴포넌트 외부로 분리할 수 있음)
(Redux와 유사한 개념이지만, 더 간단하고 로컬 상태에만 적용됩니다.)
여기서 dispatch는 발송하다,급송하다의 뜻으로 상태변화가 있어야한다는 사실을 알리는,발송하는 함수입니다.
const [state, dispatch] = useReducer(reducer, initialState);
✔ state → 현재 상태 (데이터)
✔ dispatch → 상태를 변경하는 함수 (dispatch(action)으로 실행)
✔ reducer(state, action) → 상태를 업데이트하는 함수
✔ initialState → useReducer의 초기 상태 (ex: itemData)
function reducer(state, action) {
switch (action.type) {
case "CREATE":
return [...state, action.data]; // 새로운 데이터 추가
case "EDIT":
return state.map((item) =>
item.id === action.targetId
? { ...item, ...action.data } // 특정 항목 수정
: item
);
case "DELETE":
return state.filter((item) => item.id !== action.targetId); // 특정 항목 삭제
default:
return state; // 기본적으로 기존 상태 유지
}
}
✔ state → 현재 상태 (예: itemData)
✔ action → dispatch()를 통해 전달된 객체 (예: { type: "DELETE", targetId: 2 })
dispatch({ type: "DELETE", targetId: 2 });
✔ dispatch()를 호출하면 reducer()가 실행됨
✔ reducer()는 action.type을 확인하여 state를 업데이트
✔ 업데이트된 state가 useReducer의 state로 저장됨
✔ UI가 새로운 state를 반영하여 다시 렌더링됨
🔥 이해하기 쉽게 정리하면?
코드 요소 | 역할 |
useReducer(reducer, itemData) | 상태(state)를 itemData로 초기화 |
state | 현재 상태 (데이터) |
dispatch(action) | 상태를 변경하는 요청을 보냄 |
reducer(state, action) | 요청(action)에 따라 새로운 상태를 반환 |
action.type | 어떤 작업을 수행할지 결정 (ex: "DELETE", "EDIT") |
action.data | 새로운 데이터 (ex: { date: 1234, content: "새 일기" }) |
action.targetId | 수정/삭제할 특정 항목의 ID |
<예시코드 01>
import { useReducer } from 'react';
// reducer : 변환기
// -> 상태를 실제로 변화시키는 역할
function reducer(state, action) {
console.log(state,action);
//결과: {type: "INCREASE",data: 1}
//반환되는 값이 새로운 state값으로 반영됨
switch(action.type){
case "INCREASE": return state + action.data;
case "DECREASE": return state - action.data;
default: return state;
}
function Counter() {
//dispatch : 발송하다, 급송하다
// -> 상태 변화가 있어야 한다는 사실을 알리는,발송하는 함수
const [state, dispatch] = useReducer(reducer, 0);
const onClickPlus = () => {
// 인수: 상태가 어떻게 변화되길 원하는지
//-> 액션 객체
dispatch({
type: "INCREASE",
data: 1
})
}
const onClickMinus = () => {
dispatch({
type: "DECREASE",
data: 1
})
}
return (
<div>
<h1>Count: {state}</h1>
<button onClick={onClickPlus}>+</button>
<button onClick={onClickMinus}></button>
</div>
);
}
export default Counter;
" 사용 시 주의사항 "
- useReducer는 컴포넌트 최상단이나 커스텀 Hook에서만 호출할 수 있습니다.
- 반복문이나 조건문 내에서는 사용할 수 없습니다.
- Strict Mode에서는 reducer와 init 함수가 두 번 호출됩니다. 이는 개발 환경에서만 발생하며, reducer와 init 함수가 순수 함수라면 로직에 영향을 주지 않습니다.
useContext()
useContext는 컨텍스트를 사용하여 전역 상태를 관리할 수 있는 Hook입니다.
const contextValue = useContext(MyContext);
<예시코드 01>
import { useContext } from 'react';
function MyComponent() {
const theme = useContext(ThemeContext);
return <div>{theme}</div>;
}
" 사용 시 주의사항 "
- useContext는 반드시 Context.Provider 내에서 사용해야 하며, 중복된 모듈로 인해 작동하지 않을 수 있습니다.
⭐useRef()
useRef는 컴포넌트에서 특정 DOM 요소나 값을 참조할 때 사용하는 Hook입니다.
주로 DOM 요소에 직접 접근하거나 변하지 않는 값을 저장하는 데 사용됩니다.
const refContainer = useRef(초깃값);
- initialValue: ref의 초기 값
- refContainer.current: ref가 참조하는 값이나 DOM 요소
<예시코드 01>
import { useRef } from 'react';
function MyComponent() {
const inputRef = useRef(null);
const handleFocus = () => {
inputRef.current.focus(); // DOM 요소에 접근하여 포커스를 설정
};
return (
<div>
<input ref={inputRef} />
<button onClick={handleFocus}>Focus on input</button>
</div>
);
}
" 사용 시 주의사항 "
- useRef는 값이 변경되어도 리렌더링을 발생시키지 않습니다.
- DOM 조작이나 변하지 않는 값을 저장하는 데 적합합니다.
- useRef는 컴포넌트가 리렌더링 될 때도 같은 ref 객체를 계속 사용합니다.
- useRef의 초기값은 **null**로 설정되며, DOM 요소가 마운트 되지 않으면 null을 가집니다.
마무리
React의 Hooks는 함수형 컴포넌트에서 상태와 생명주기를 쉽게 관리할 수 있게 해 줍니다.
이를 통해 코드가 더 간결하고 효율적이며, 유지보수도 쉬워집니다.
useState, useEffect, useReducer, useContext 등의 Hooks를 잘 활용하면, 복잡한 상태 관리와 데이터 흐름을 더 직관적으로 처리할 수 있습니다. 다만, 사용 시 주의사항을 잘 지켜야 성능과 효율성을 극대화할 수 있습니다.
더 많은 정보는 React 공식 문서에서 확인할 수 있습니다.