오늘은 React에서 유용하게 쓸 수 있는 Hook에 대해 알아보려한다.
사실 뭣도 모를 때 useState와 useEffect만 있는 줄 알았는데... (그것도 사실 뭐하는 용도인지 잘 몰랐음)
실무에서 처음보는 use접두사가 붙은 것들이 꽤 있어서 어버버했었다.
그 뒤로 천천히 알아가며 개념을 익혔는데 다시 한 번 정리하고자 티스토리에 기록하려함!!
React Hooks
🤔 React Hooks?
React 버전 16.8부터 도입된 개념으로, 함수형 컴포넌트에서도 상태 관리와 다양한 React 기능을 사용할 수 있도록 해줍니다. 이전에는 클래스형 컴포넌트에서만 상태 관리와 라이프사이클 메서드를 사용할 수 있었지만, Hook을 사용하면 함수형 컴포넌트에서도 이러한 기능을 사용할 수 있게 되었습니다.
🤔 함수형 컴포넌트와 클래스형 컴포넌트의 차이는 뭔데?
👀 함수형 컴포넌트
함수형 컴포넌트는 함수를 사용하여 컴포넌트를 정의하는 방식입니다. React Hooks가 도입되기 이전에는 주로 간단한 상태가 없는 컴포넌트나, stateless 컴포넌트로 사용되었습니다. 하지만 React Hooks를 사용하면 함수형 컴포넌트에서도 상태와 생명주기를 관리할 수 있게 되었습니다.
아마 React를 쓰는 분들은 거의 함수형 컴포넌트를 쓰지 않을까 싶다.
import React, { useState } from 'react';
function MyFunctionComponent() {
const [count, setCount] = useState(0);
const incrementCount = () => {
setCount(count + 1);
}
return (
<div>
<p>Count: {count}</p>
<button onClick={incrementCount}>Increment</button>
</div>
);
}
👀 클래스형 컴포넌트
클래스형 컴포넌트는 React에서 초기부터 사용되던 방식입니다. ES6 클래스 문법을 사용하여 React.Component 클래스를 상속받아서 컴포넌트를 정의합니다. 상태(state)를 관리하기 위해 this.state를 사용하고, 라이프사이클 메서드를 사용하여 컴포넌트의 생명주기를 관리합니다.
아무래도 class 문법이 익숙하지 않은 사람들에게 어려울 수 있다!
import React, { Component } from 'react';
class MyClassComponent extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
incrementCount = () => {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.incrementCount}>Increment</button>
</div>
);
}
}
🔍 React Hooks의 종류
- useState
- useEffect
- useContext
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeHandle
- useLayoutEffect
- useDebugValue
사실 밑의 세 개는 실무나 프로젝트 했을 때 본 적은 없다.
(그만큼 안 쓰이나..?)
1편에서는 useState~useReducer만 정리하고
2편에는 나머지들을 정리해볼 예정이다.
useState
useState는 React의 내장 Hook 중 하나로, 함수형 컴포넌트에서 상태를 관리할 수 있도록 해줍니다. 이 Hook을 사용하면 컴포넌트 내에서 상태를 지역적으로 관리할 수 있습니다.
import React, { useState } from 'react';
function ExampleComponent() {
// useState를 사용하여 상태를 선언!
const [count, setCount] = useState(0);
// 상태를 업데이트하는 함수를 정의!
const incrementCount = () => {
setCount(count + 1);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={incrementCount}>Click me</button>
</div>
);
}
클릭을 할 때마다 숫자가 증가하는 로직이다. 보통 useState를 쓸 때는 위에 import로 선언하고 사용해야한다.
그리고 배열을 반환하기 때문데 [상태 값, 상태 변경 요소]로 설정 해주아야한다. 그리고 보통은 변경 요소 앞에 set-접두사를 붙여준다.
[count, setCount]처럼!
또한 useState()안에 들어가는 건 초기 상태값인데 어떠한 자료형이든 들어갈 수 있다. (숫자, 문자열, 불리언, 객체, 배열, null, undefined 등등등..)
하지만 useState를 쓸 때는 주의할 점이 있다!
💥 useState 주의점
useState는 상태를 관리하고, 상태가 변경되면 해당 컴포넌트가 다시 렌더링되기 때문에, 상태를 렌더링에 영향을 줄 수 있습니다.
- 무한 루프에 빠지지 않도록 주의: useState를 사용하여 상태를 업데이트할 때, 상태 변경이 렌더링을 유발하고, 다시 렌더링은 상태를 다시 확인하여 업데이트를 유발할 수 있습니다. 이런 과정이 끊임없이 반복되면 무한 루프에 빠질 수 있습니다. 이를 방지하기 위해 useEffect나 useCallback 등을 사용하여 상태 변경에 따라 불필요한 렌더링이 발생하지 않도록 조치해야 합니다.
- 렌더링 비용 최적화: 상태를 렌더링할 때는 해당 상태가 렌더링에 어떤 영향을 미치는지 고려해야 합니다. 불필요한 상태 변경을 줄이고, 상태 업데이트를 최적화하여 성능을 향상시키는 것이 중요합니다. 또한, useState를 사용하여 렌더링할 때는 상태 변경이 해당 컴포넌트뿐만 아니라 하위 컴포넌트에도 영향을 미치는지 고려해야 합니다.
- 상태 변경 시 렌더링 횟수 제어: 상태 변경에 따라 렌더링이 여러 번 발생하는 것을 방지하기 위해, 상태 변경이 발생하는 곳에서 최적화를 고려해야 합니다. 예를 들어, useState를 사용하여 상태를 업데이트하는 함수를 useCallback으로 감싸서 불필요한 렌더링을 방지할 수 있습니다.
사실 상태관리가 되게 까다롭기 때문에 요즘은 상태 관리 라이브러리들이 굉장히 잘 나와있다. (Redux, Zustand 등등등..)
useState로 상태 관리하는데 개고생을 한 번 하고나면 라이브러리들의 중요성을 뼈저리게 느낄 것이다,,,(내가 그랫음)
useEffect
useEffect는 함수형 컴포넌트에서 부수 효과를 처리하기 위해 사용됩니다. 부수 효과란 컴포넌트 내부에서 이루어지는, 상태 변경이나 외부와의 상호작용과 같은 작업을 말합니다. useEffect를 사용하면 컴포넌트의 생명주기와 관련된 동작을 정의할 수 있습니다.
import React, { useState, useEffect } from 'react';
function ExampleComponent() {
const [count, setCount] = useState(0);
// useEffect를 사용하여 부수 효과 정의
useEffect(() => {
// 컴포넌트가 렌더링될 때마다 실행됨
console.log('컴포넌트가 렌더링되었습니다.');
// count 상태가 변경될 때마다 실행됨
console.log('현재 count 값:', count);
// 부수 효과를 정리하는 함수 반환
return () => {
console.log('컴포넌트가 언마운트되기 전에 실행됩니다.');
};
}, [count]); // 의존성 배열에 count를 지정하여 count 값이 변경될 때만 useEffect가 실행됨
const incrementCount = () => {
setCount(prevCount => prevCount + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={incrementCount}>Increment</button>
</div>
);
}
이 예제에서는 useState를 사용하여 count라는 상태를 정의하고, 버튼을 클릭할 때마다 count를 증가시키 incrementCount 함수를 정의했습니다. useEffect를 사용하여 컴포넌트의 렌더링과 count 상태의 변경을 감지하고, 각각에 대해 적절한 로그를 출력합니다.
useEffect의 의존성 배열에 count를 전달함으로써 count 값이 변경될 때마다 useEffect 콜백 함수가 실행되어 콘솔에 해당 값이 출력됩니다. 또한, 부수 효과 정리 함수를 반환하여 컴포넌트가 언마운트되기 전에 실행되는 로그도 출력됩니다
역시나 useEffect도 주의할 점이 있는데,,
💥 useEffect 주의점
- 의존성 배열의 사용: useEffect에서 의존성 배열을 지정하여 특정 상태나 프로퍼티가 변경될 때만 부수 효과를 실행할 수 있습니다. 이를 통해 불필요한 부수 효과 실행을 방지할 수 있습니다.
- 정리 함수 반환: useEffect 안에서 부수 효과를 정의할 때는 옵셔널로 정리(clean-up) 함수를 반환할 수 있습니다. 이 함수는 컴포넌트가 언마운트되거나 업데이트되기 직전에 실행되어 리소스를 정리하거나 구독을 해제하는 등의 작업을 수행합니다.
- 비동기 작업 처리: useEffect 안에서 비동기 작업을 처리할 때는 콜백 함수를 async 함수로 정의하거나, 콜백 함수 내에서 async 함수를 호출하는 방식으로 처리할 수 있습니다. 단, useEffect 자체는 async 함수를 지원하지 않으므로 이에 유의해야 합니다.
- 의존성 배열의 사용에 대한 경고: 의존성 배열을 사용할 때 모든 의존성을 배열에 포함시키는 것이 중요합니다. 의존성을 누락하면 부수 효과가 예상대로 동작하지 않을 수 있습니다.
useContext
useContext는 컴포넌트 트리 전체에서 전역적으로 데이터를 공유할 수 있도록 도와주는 기능입니다. 주로 상태 관리 라이브러리(예: Redux)를 대체하는 데 사용될 수 있습니다.
useContext를 쓰려면 단계가 있다!
1. Context 생성: createContext 함수를 사용하여 새로운 Context 객체를 생성합니다. createContext 함수의 인자로는 해당 Context의 기본 값을 전달할 수 있습니다.
import React from 'react';
const MyContext = React.createContext(defaultValue);
2. Provider 제공: Context.Provider 컴포넌트를 사용하여 데이터를 제공합니다. Provider는 컴포넌트 트리에서 Context 객체를 사용하는 모든 하위 컴포넌트에 대해 해당 Context의 값을 전달합니다.
<MyContext.Provider value={/* value */}>
{/* 하위 컴포넌트 */}
</MyContext.Provider>
3. Consumer 사용: useContext Hook을 사용하여 Context 값을 가져올 수 있습니다. useContext의 인자로는 해당 Context 객체를 전달하며, 이를 통해 해당 Context의 값에 접근할 수 있습니다.
import React, { useContext } from 'react';
const value = useContext(MyContext);
4. 값 업데이트: Context 값을 업데이트하려면 Provider의 value prop을 변경하여 새로운 값을 제공해야 합니다. 이러한 변경은 Provider를 감싸는 상위 컴포넌트에서 이루어져야 합니다.
<MyContext.Provider value={newValue}>
{/* 하위 컴포넌트 */}
</MyContext.Provider>
단계만 보면 와닿지 않으니 예제를 작성해보겠다.
createContext와 useContext를 사용하여 테마 변경 기능을 구현해볼 것임!!
1. 테마 Context 생성
import React, { createContext, useContext, useState } from 'react';
// 기본 테마 값
const themes = {
light: {
foreground: '#000000',
background: '#eeeeee',
},
dark: {
foreground: '#ffffff',
background: '#222222',
},
};
const ThemeContext = createContext(themes.light);
2. ThemeProvider 제공하기
const App = () => {
const [theme, setTheme] = useState(themes.light);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<div style={{ background: theme.background, color: theme.foreground }}>
<h1>Theme Example</h1>
<ThemedButton />
</div>
</ThemeContext.Provider>
);
};
3. 테마 사용하기
const ThemedButton = () => {
const { theme, setTheme } = useContext(ThemeContext);
const toggleTheme = () => {
setTheme(theme === themes.light ? themes.dark : themes.light);
};
return (
<button
onClick={toggleTheme}
style={{
background: theme.background,
color: theme.foreground,
border: `2px solid ${theme.foreground}`,
borderRadius: '5px',
padding: '10px',
cursor: 'pointer',
}}
>
Toggle Theme
</button>
);
};
위 코드에서는 기본 테마 값을 light로 설정하고, ThemedButton을 클릭할 때마다 테마를 변경할 수 있도록 구현되어 있다. ThemedButton 컴포넌트는 useContext를 사용하여 ThemeContext에서 테마 값을 가져와서 버튼 스타일을 변경하는 로직이다.
useContext를 쓰다가 유튜브나 구글링을 해보면 Context API를 볼 수 있을 것이다.나중에 다룰 예정이지만 간단하게 설명하자면 Context API는 React에서 전역 상태를 관리하기 위한 메커니즘을 제공해주는 것이라고 생각하면 쉽다.
💥 useContext 주의점
- 컴포넌트 구조와 의존성 관리: useContext를 사용하여 컴포넌트에서 Context를 가져올 때, 컴포넌트의 구조와 의존성을 고려해야 합니다. Context를 너무 많이 사용하면 컴포넌트 간의 의존성이 복잡해질 수 있습니다. 필요한 경우에만 useContext를 사용하여 컴포넌트 간의 의존성을 최소화해야 합니다.
- Context 변경 주기와 성능 최적화: Context 값이 변경될 때마다 컴포넌트가 다시 렌더링되기 때문에, 불필요한 렌더링을 방지하고 성능을 최적화해야 합니다. 필요한 경우에만 useContext를 사용하여 컴포넌트를 업데이트하도록 조치해야 합니다.
- Context 의존성 배열 사용: useContext를 사용할 때 의존성 배열을 지정하여 컴포넌트가 필요한 경우에만 다시 렌더링되도록 할 수 있습니다. 의존성 배열을 사용하여 컴포넌트가 Context 값의 변경에 의존하는 경우에만 해당 컴포넌트가 업데이트되도록 설정할 수 있습니다.
- 컨텍스트 타입과 타입 안정성: useContext를 사용할 때 올바른 컨텍스트 타입을 사용하고, 타입 안정성을 유지해야 합니다. TypeScript를 사용하는 경우에는 useContext를 사용할 때 제대로 된 타입을 지정하여 타입 안정성을 보장해야 합니다.
- 가독성 유지: useContext를 사용하여 코드를 간결하고 가독성 있게 유지해야 합니다. 필요한 경우 커스텀 훅을 만들어서 로직을 추상화하고 재사용할 수 있도록 하면 좋습니다.
useReducer
useReducer는 React의 Hook 중 하나로, 상태를 관리하기 위한 기능입니다. 클래스형 컴포넌트에서 사용되는 useState와 유사한 역할을 하지만, 좀 더 복잡한 상태 로직을 다룰 때 유용합니다. 주로 컴포넌트에서 여러 상태를 하나로 묶거나, 상태를 업데이트하는 로직을 분리하고자 할 때 사용됩니다.
const [state, dispatch] = useReducer(reducer, initialState);
여기서 reducer는 상태를 업데이트하는 함수이고, initialState는 초기 상태이다!
useReducer는 현재 상태(state)와 상태를 업데이트하는 함수(dispatch)를 반환하는데, dispatch 함수를 사용하여 상태를 업데이트할 수 있다.
reducer 함수는 현재 상태와 업데이트에 필요한 액션(action)을 받아서 새로운 상태를 반환한다. 액션은 보통 객체 형태이며, type 속성을 가져야 한다. 보통 switch 문을 사용하여 액션의 타입에 따라 다른 동작을 수행!
간단한 예제와 같이 살펴보자.
import React, { useReducer } from 'react';
// reducer 함수 정의
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
default:
return state;
}
};
const Counter = () => {
// 초기 상태 정의
const initialState = { count: 0 };
// useReducer를 사용하여 상태와 dispatch 함수를 생성
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
{/* dispatch 함수를 사용하여 액션을 디스패치 */}
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
</div>
);
};
export default Counter;
이 예제에서는 reducer 함수를 사용하여 count 상태를 업데이트하는 로직을 정의하고, useReducer를 사용하여 상태(state)와 dispatch 함수를 생성합니다. dispatch 함수를 사용하여 'increment' 액션을 디스패치하면 count가 증가하게 됩니다.
요건 간단한 예제고 조금 더 복잡한? (복잡한 것도 아니지만..)
투두리스트를 예제로 들 수 있다.
import React, { useReducer, useState } from 'react';
// reducer 함수 정의
const todoReducer = (state, action) => {
switch (action.type) {
case 'add':
return [...state, { id: Date.now(), text: action.payload }];
case 'remove':
return state.filter(todo => todo.id !== action.payload);
default:
return state;
}
};
const TodoList = () => {
// useReducer를 사용하여 상태와 dispatch 함수 생성
const [todos, dispatch] = useReducer(todoReducer, []);
const [text, setText] = useState('');
// 투두 아이템 추가
const addTodo = () => {
dispatch({ type: 'add', payload: text });
setText('');
};
// 투두 아이템 삭제
const removeTodo = id => {
dispatch({ type: 'remove', payload: id });
};
return (
<div>
<input
type="text"
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={addTodo}>Add Todo</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => removeTodo(todo.id)}>Remove</button>
</li>
))}
</ul>
</div>
);
};
export default TodoList;
이 예제에서는 useReducer를 사용하여 투두 리스트의 상태와 상태를 업데이트하는 로직을 관리합니다. addTodo 함수를 사용하여 새로운 투두 아이템을 추가하고, removeTodo 함수를 사용하여 특정 투두 아이템을 삭제합니다. 상태를 변경하는 모든 작업은 dispatch 함수를 사용하여 수행됩니다.
💥 useReducer 주의점
- 컴포넌트 분할: 상태와 상태를 업데이트하는 로직을 하나의 컴포넌트에서 모두 다루는 것이 아니라, 관련된 기능을 분리하여 여러 컴포넌트로 나누는 것이 좋습니다. 이렇게 하면 컴포넌트의 역할이 명확해지고, 유지보수가 쉬워집니다.
- Reducer 함수 분리: reducer 함수는 상태를 업데이트하는 로직을 담당합니다. 복잡한 상태 로직을 처리할 때는 reducer 함수를 분리하여 여러 개의 작은 함수로 나누는 것이 좋습니다. 이렇게 하면 코드를 이해하기 쉽고, 테스트하기도 쉽습니다.
- 불변성 유지: reducer 함수에서 상태를 업데이트할 때는 기존 상태를 변경하지 않고 새로운 상태를 반환해야 합니다. 이를 위해 불변성을 유지하는 방법을 사용해야 합니다. 예를 들어, 배열을 업데이트할 때는 배열 메서드의 변형 함수 대신 새로운 배열을 생성하는 방법을 사용해야 합니다.
- 의존성 배열 사용: useReducer를 사용할 때 의존성 배열을 지정하여 필요한 경우에만 컴포넌트가 다시 렌더링되도록 설정할 수 있습니다. 의존성 배열을 지정하지 않으면 컴포넌트가 불필요하게 다시 렌더링될 수 있으므로 주의해야 합니다.