React Hooks
- React v16.8에서 도입된 새로운 기능
- 함수형 컴포넌트에서 상태와 생명주기 기능을 사용할 수 있게 함
- 이전에는 클래스 컴포넌트에서만 상태를 가질 수 있었음
클래스 기반 컴포넌트의 단점
- 컴포넌트 간 상태 로직을 재사용하기 어려움
- 비슷한 형태의 상태 로직을 각 컴포넌트에서 직접 작성해야 했음
- 생명주기 메서드에서 서로 관련 없는 로직들이 얽혀 코드의 복잡성을 증가시키는 문제가 있었음
- 생명주기 메서드란? 컴포넌트의 특정 시점에 호출되는 메서드
- componentDidMount: 컴포넌트가 마운트된 직후에 호출
- componentDidUpdate: 컴포넌트가 업데이트된 직후에 호출
- 위와 같은 생명주기 메서드에서만 상태 업데이트에 대한 사이드 이펙트를 처리할 수 있었음
- 생명주기 메서드란? 컴포넌트의 특정 시점에 호출되는 메서드
정리하자면 클래스형 컴포넌트는 프로젝트 규모가 커졌을 때, 상태를 스토어에 연결하거나 비슷한 로직을 가진 상태 업데이트 및 사이드 이펙트 처리하기가 불편했다.
💡 함수형 컴포넌트의 시대 도래
- Hooks의 도입으로 인해 함수형 컴포넌트에서도 컴포넌트의 생명주기 로직을 쉽게 실행할 수 있게 됨
- 이에 따라 복잡한 클래스형 컴포넌트에서 벗어나 간결하고 재사용 가능한 함수형 컴포넌트가 보편화 됨
💡 Hooks의 장점
- 비즈니스 로직을 재사용하거나 작은 단위로 코드를 분할하여 테스트하는게 용이해짐
- 사이드 이펙트와 상태를 관심사에 맞게 분리하여 구성할 수 있게 됨
useState
상태를 관리하기 위해 사용
const [state, setState] = useState(initialState);
useEffect
사이드 이펙트를 수행하기 위해 사용
컴포넌트가 마운트되거나 업데이트 될 때 어떤 일을 수행해야 하는지 알려주기 위해 사용
useEffect(() => {
// 사이드 이펙트 코드
// 주로 API 호출, 이벤트 리스너 등록, 타이머 설정 등을 수행
return () => {
// 클린업 코드: 이전 사이드 이펙트에서 설정한 리소스 정리
// 주로 이벤트 리스너 해제, 타이머 해제 등을 수행
}
}, [dependencies]); // 의존성 배열의 값이 변경될 때마다 useEffect 재실행
// 빈 배열([])일 경우, 컴포넌트가 처음 마운트될 때 한 번만 useEffect 실행
useContext
Context API를 통해 전역 상태를 사용
const value = useContext(MyContext);
- Context 생성: React.createContext() 를 사용하여 Context 객체를 생성
- Provider 설정: Context.Provier 를 사용하여 하위 컴포넌트들에게 전역 상태를 제공. Provider 는 value prop을 통해 제공할 데이터를 지정
- Context 소비: useContext 훅을 사용해 필요한 컴포넌트에서 Context의 값을 소비
import React, {useContext} from 'react';
// 1. Context 객체 생성: 컴포넌트 트리 전체에 MyContext를 전달할 수 있는 방법 제공
const MyContext = React.createContext();
function MyComponent() {
// 3. useContext 훅 사용: MyContext의 현재 값 가져오기 (Provider가 제공한 value)
const value = useContext(MyContext);
return <div>{value}</div>;
}
function App(){
// 2. Context.Provider 사용: MyContext.Provider를 통해 하위 컴포넌트에 value 제공
return (
<MyContext.Provider value="Hello, World!">
<MyComponent />
</MyContext.Provider>
);
}
useReducer
복잡한 상태 관리를 위해 사용
상태와 상태를 업데이트하는 로직을 분리하여, 여러 상태 값이 복잡하게 얽혀 있는 상황에서 명확하고 구조적인 접근을 제공하기 위해 사용
const [state, dispatch] = useReducer(reducer, initialState);
- 초기 상태 정의: 초기 상태 객체를 정의
- 리듀서 함수 정의: 상태와 액션을 받아 새로운 상태를 반환하는 함수
- useReducer 훅 사용: useReducer 훅을 사용하여 상태와 디스패치 함수를 얻음. 리듀서 함수와 초기 상태를 전달
import React, { useReducer } from 'react';
// 1. 초기 상태 정의
const initialState = { count: 0 };
// 2. 리듀서 함수 정의: 상태와 액션을 받아 새로운 상태를 반환하는 함수
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
// 3. useReducer 훅을 통해 상태(initialState)와 디스패치 함수(reducer)를 얻음
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<p>Count: {state.count}</p>
{/* 버튼 클릭 시 'increment' 액션을 디스패치하여 업데이트 */}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
{/* 버튼 클릭 시 'decrement' 액션을 디스패치하여 업데이트 */}
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</>
);
}
Context API + useReducer
- 추가 라이브러리를 필요로 하지 않는 React 상태 관리 방법
- useContext 훅와 useReducer 훅을 함께 사용
- 복잡한 상태 관리에는 한계가 있기 때문에 최근에는 이 방법 대신 추가 라이브러리를 사용하는 추세
// App.js
import React, { useReducer, useContext, createContext } from 'react';
// 1. Context 객체 생성
const CountContext = createContext();
// 2. 리듀서 함수 정의
const reducer = (state, action) => {
switch (action.type) {
case 'incremet':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error;
}
};
// 3. Provider 설정
const CountProvider = ({ children }) => {
// 4. useReducer 훅을 통해 상태와 디스패치 함수 가져오기
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<CountContext.Provider value={{ state, dispatch }}>
{children}
</CountContext.Provider>
);
};
const Counter = () => {
// 5. useContext 훅을 통해 Context의 현재 값을 가져오기
const { state, dispatch } = useContext(CountContext);
return (
<>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</>
);
};
const App = () => (
<CountProvider>
<Counter />
</CountProvider>
);
export default App;
useMemo
메모이제이션된 값을 반환
이전에 생성된 값 또는 결과를 기억하며, 동일한 값과 결과를 반복해서 생성해주지 않도록 하기 위해 사용
성능 최적화를 위해 사용
const memoizedValue = useMemo(() => {
return a + b;
}, [a, b]);
import React, { useMomo, useState } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
const [otherValue, setOtherValue] = useState(0);
// useMemo 훅을 사용하여 의존성 배열의 값(count)이 변경되지 않으면 이전 값 재사용
const calcutateTotal = useMemo(() => {
console.log("계산 중");
let total = 0;
for (let i = 0; i < 10000000; i++) {
total += count;
}
return total;
}, [count]);
return (
<>
<p>Count: {count}</p>
<p>Computed Total: {calculateTotal}</p>
{/* Increment Count 버튼 클릭 시, count 값이 증가하여 useMemo가 재실행됨 */}
<button onClick={() => setCount(count + 1)}>Increment Count</button>
{/* Increment Other Value 버튼 클릭 시, otherValue 값이 증가하지만, useMemo는 재실행되지 않음 */}
<button onClick={() => setOtherValue(otherValue + 1)}>Increment Other Value</button>
</>
);
}
더보기
만일 useMemo를 사용하지 않으면?
- 두 버튼을 차례로 한 번씩 클릭한다면?
- useMemo 사용 예제
- 최초 렌더링 : "계산 중" "Count: 0" "Computed Total: 0"
- Increment Count 버튼 클릭 시 : "계산 중" "Count: 1" "Computed Total: 10000000"
- Increment Other Value 버튼 클릭 시 : "Count: 1" "Computed Total: 10000000"
- 일반 함수 사용 예제
- 최초 렌더링 : "계산 중" "Count: 0" "Computed Total: 0"
- Increment Count 버튼 클릭 시 : "계산 중" "Count: 1" "Computed Total: 10000000"
- Increment Other Value 버튼 클릭 시 : "계산 중" "Count: 1" "Computed Total: 10000000"
- useMemo 사용 예제
import React, { useState } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
const [otherValue, setOtherValue] = useState(0);
// useMemo를 사용하지 않고 직접 계산
const calculateTotal = () => {
console.log("계산 중");
let total = 0;
for (let i = 0; i < 10000000; i++) {
total += count;
}
return total;
};
return (
<>
<p>Count: {count}</p>
<p>Computed Total: {calculateTotal()}</p>
{/* Increment Count 버튼 클릭 시, count 값이 증가하여 컴포넌트가 다시 렌더링됨 */}
<button onClick={() => setCount(count + 1)}>Increment Count</button>
{/* Increment Other Value 버튼 클릭 시, otherValue 값이 증가하여 컴포넌트가 다시 렌더링됨 */}
<button onClick={() => setOtherValue(otherValue + 1)}>Increment Other Value</button>
</>
);
}
export default MyComponent;
💡 Memoization
기존에 수행한 연산의 결과 값을 어딘가에 저장해두고 동일한 입력이 들어오면 재활용하는 프로그래밍 기법
메모리를 조금 더 사용하지만 반복 연산을 피할 수 있기 때문에 성능을 최적화 할 수 있음
useCallback
메모이제이션된 함수를 반환
useMemo 와 동일하게 성능 최적화를 위해 사용
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
import React, { useCallback, useState } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
// useCallback 훅을 사용하여 의존성 배열의 값이 변경되지 않으면 이전 함수 재사용
const handleClick = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []);
return (
<>
<p>{count}</p>
{/* 버튼 클릭 시 handleClick 함수가 호출되어 count 값이 증가함 */}
<button onClick={handleClick}>+</button>
</>
);
}
💡 useMemo vs useCallback
- useMemo
- 메모이제이션된 값
- 계산 비용이 큰 작업에 최적화하며 렌더링 성능을 최적화 함
- useCallback
- 메모이제이션된 콜백 함수
- 불필요한 함수 재생성을 방지하여, 자식 컴포넌트에 전달할 콜백 함수를 최적화
- 자식 컴포넌트의 불필요한 렌더링을 방지함
useRef
변경 가능한 ref 객체를 반환
const refContainer = useRef(initialValue);
import React, { useRef } from "react";
function TextInputWithFocusButton() {
const inputEI = useRef(null); // ref 객체를 우선 null로 선언
const onButtonClick = () => {
inputEI.current.focus();
}
return (
<>
{/* input을 ref 객체로 할당 */}
<input ref={inputEI} type="text" />
{/* 버튼 클릭 시 onButtonClick 함수가 호출되어 위의 input이 포커싱 됨 */}
<button onClick={onButtonClick}>포커싱</button>
</>
);
};
useLayoutEffect
모든 DOM이 변경된 후 동기적으로 실행
useLayoutEffect(() => {
// DOM 업데이트 이후 실행할 코드 작성
}, [dependencies]); // 의존성 배열의 값이 변경될 때마다 useLayoutEffect 재실행
import React, { useState, useLayoutEffect } from 'react';
function ResizeableBox() {
const [width, setWidth] = useState(window.innerWidth);
// useLayoutEffect를 이용하여 윈도우 사이즈가 변경될 때 마다 width 상태 업데이트
useLayoutEffect(() => {
const handleResize = () => {
setWidth(window.innerWidth);
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize); // 클린업 : 언마운트 될 때 리스너 제거
};
}, []);
return (
<div>
<h2>Resizeable Box</h2>
{/* 브라우저 창의 사이즈를 변경할 때마다 width 값 변경 */}
<p>Current Width: {width}px</p>
</div>
);
};
export default ResizeableBox;
💡 useEffect vs useLayoutEffect
- useEffect
- 비동기적으로 실행
- 컴포넌트 렌더링 후 실행
- 비동기 작업, 데이터 가져오기, 구독 설정 등의 목적으로 사용 (ex. API 호출)
- useCallback
- 동기적으로 실행
- 모든 DOM 변경 후 실행
- 레이아웃 측정, DOM 업데이트 등의 목적으로 사용 (ex. 크기 측정)
'REACT' 카테고리의 다른 글
[JAVASCRIPT][REACT] API 데이터 가져오기 (GET) (1) | 2023.11.26 |
---|---|
[REACT] React Query(TanStack Query) 직접 적용해보며 알아가기 (0) | 2023.11.14 |
[REACT] createGlobalStyle 안에서 @import 사용 경고 해결하기 (0) | 2023.10.01 |
[REACT] TDD 와 리액트 테스트 도구 (0) | 2023.09.15 |
[REACT] ReactDOM.render 과 ReactDOM.createRoot 의 차이점 (0) | 2023.05.17 |