React Hooks란?
React Component는 클래스형 컴포넌트와 함수형 컴포넌트 두 가지가 존재합니다.
함수형 컴포넌트는 클래스형 컴포넌트보다 코드도 간결하고, 더 빠른 성능이 나옵니다. 그럼에도 예전엔 클래스형 컴포넌트를 더 많이 사용했었습니다. 그 이유는 상태 관리와 라이프사이클 메소드를 클래스형 컴포넌트에서만 사용할 수 있었기 때문입니다.
하지만 React 16.8부터 리액트 Hooks가 발표되고 함수형 컴포넌트에서 Hooks를 사용하여 상태 관리와 사이트 이펙트를 처리할 수 있게 되어, 현재는 함수형 컴포넌트를 대다수가 사용합니다.
React 라이프 사이클
주요 React Hooks
- useState
- useEffect
- useContext
- useReducer
- useCallback
- useMemo
- useRef
- useLayoutEffect
- useDebugValue
1. useState
useState는 함수형 컴포넌트에서 상태를 관리할 수 있게 해주는 Hook입니다.
// useState(""): value와 setValue를 반환하고, 초기 state 값을 정하는 Hook
// value: 변수 이름, getter
// setValue: setter
const [value, setValue] = useState("");
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}
export default Counter;
2. useEffect
useEffect는 함수형 컴포넌트에서 사이트 이펙트를 수행할 수 있게 해주는 Hook입니다. 리액트 컴포넌트가 렌더링 될 때마다 특정 작업을 수행할 수 있도록 하는 Hook으로 클래스형 컴포넌트의 생명 주기 메소드(componentDidMount, componentDidUpdate, componentWillUnmount)를 대체합니다.
- 컴포넌트가 마운트 됐을 때
- 컴포넌트가 언마운트 됐을 때
- 컴포넌트가 업데이트 될
useEffect(() => {실행할 로직}, [의존성 배열])
의존성 배열은 useEffect 내부에서 사용하는 외부 상태나 프로퍼티를 의미합니다. 해당 배열의 값이 변경되면 useEffect가 실행됩니다. 만약 두 번째 인자를 넣지 않는다면, 매 렌더링마다 실행됩니다.
useEffect(() => {
// 렌더링 될 때마다 실행
});
useEffect(() => {
// 처음 렌더링 될 때만 실행
}, []);
useEffect(() => {
// 처음 렌더링 될 때 실행
// data가 변경되어 컴포넌트가 재렌더링 될 때마다 실행
}, [data]);
clean-up 함수
`useEffect`에서는 함수를 반환할 수 있는데 이를 clean-up 함수라고 합니다. clean-up 함수를 언제 사용할 수 있을까요?
`useEffect`에서 특정 작업이 완료되지 않거나 지속적으로 실행되면 메모리 누수가 발생할 수 있습니다. 예를 들어, setInterval을 통한 반복 작업이나 setTimeout을 통한 작업 예약, 구독, 네트워크 요청, 이벤트 리스너 등과 같은 경우 입니다. clean-up 함수를 사용하면 이러한 리소스를 정리하여 메모리 누수를 방지할 수 있습니다.
다음 코드는 메모리를 해제하지 않아 점점 더 많은 메모리를 차지하게 되는 상황입니다. 예를 들어, 컴포넌트가 언마운트된 후에도 타이머나 이벤트 리스너가 계속 실행되면 메모리 누수가 발생할 수 있습니다.
메모리 누수 코드
import React, { useState, useEffect } from 'react';
function TimerComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
// clean-up 함수가 없음
}, []); // 의존성 배열이 빈 배열
return <div>Count: {count}</div>;
}
export default TimerComponent;
위 코드에서 setInterval로 설정된 타이머는 컴포넌트가 언마운트된 후에도 계속 실행됩니다. 이로 인해 메모리 누수가 발생할 수 있습니다. 다음은 useEffect에서 clean-up 함수를 사용하여 타이머를 정리한(메모리 누수를 방지) 코드입니다.
메모리 누수 방지 코드
import React, { useState, useEffect } from 'react';
function TimerComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
// clean-up 함수
return () => {
clearInterval(timer);
};
}, []); // 의존성 배열이 빈 배열
return <div>Count: {count}</div>;
}
export default TimerComponent;
useEffect 내부에서 타이머를 설정하는 코드와 함께 return을 사용하여 clean-up함수를 정의하였습니다. clean-up함수로 clearInterval을 호출하여 타이머를 정리하였는데, 이 함수는 컴포넌트가 언마운트되기 직전에 호출됩니다.
useEffect에서 clean-up함수가 실행되는 경우
1. 의존생 배열이 비어있는 경우: 컴포넌트가 언마운트될 때
2. 의존생 배열에 데이터가 있는 경우: useEffect가 다시 실행되기 직전
위 메모리 누수 방지 코드 예제는 1.컴포넌트가 어마운트될 때인 경우에 해당합니다. 의존성 배열이 빈 배열이기 때문에, `useEffect`는 컴포넌트가 처음 마운트될 때 한 번 실행되고, 컴포넌트가 언마운트될 때 clean-up함수 즉, clearInterval(timer)가 호출되어 타이머가 정리됩니다.
useEffect의 두 번째 매개변수로 전달되는 의존성 배열에 따라 처음 렌더링 시에 실행되는지, 매번 실행 되는지, 값이 변경 될 때마다 실행되는지가 결정됩니다. 때문에 clean-up 함수의 실행 시점도 이에 따라 다릅니다.
useEffect(() => {
// 렌더링 될 때마다 실행됩니다.
// 모든 렌더링 후에 `useEffect`가 실행되고, 그 직전에 clean-up 함수가 실행됩니다.
});
useEffect(() => {
// 처음 렌더링 될 때만 실행됩니다.
// `userEffect`가 컴포넌트가 처음 마운트될 때 한 번 실행되고,
// 컴포넌트가 언마운트될 때 clean-up 함수가 실행됩니다.
}, []);
useEffect(() => {
// 처음 렌더링 될 때 실행
// data가 변경되어 컴포넌트가 재렌더링 될 때마다 실행
// `useEffect`가 의존성 배열 값인 data가 변경될 때마다 실행되고,
// 그 직전에 clean-up 함수가 실행됩니다.
}, [data]);
3. useContext
useContext는 컨텍스트를 함수형 컴포넌트에서 사용할 수 있게 해줍니다. 데이터를 전역적으로 공유하고 싶은 경우에 유용합니다.
import React, { createContext, useContext } from 'react';
const ThemeContext = createContext('kim'); // 기본값 지정
function App() {
return (
<ThemeContext.Provider value="yeo">
<Themed />
</ThemeContext.Provider>
);
}
function Themed() {
const theme = useContext(ThemeContext);
return <div>The current theme is {theme}</div>;
}
export default App;
createContext의 파라미터에는 Context의 기본값을 설정할 수 있습니다. Context를 사용할 때 값을 따로 지정하지 않을 경우 사용되는 기본값입니다. Context안에 Provider라는 컴포넌트가 들어있는데, 이 컴포넌트를 통해 Context의 값을 정할 수 있습니다. 이때 value 속성을 이용해주면 됩니다. 위처럼 Provider에 의해 감싸진 컴포넌트는 Context의 값을 바로 조회해서 사용할 수 있습니다.
4.useReducer
useReducer는 상태 관리 로직이 복잡할 때 사용하는 Hook입니다. 상태를 관리할 때 useState를 사용해서 상태를 설정해주었는데, useReducer를 사용해도 상태를 관리할 수 있습니다. 이 Hook 함수는 컴포넌트의 상태 업데이트 로직을 컴포넌트에서 분리시킬 수도 있습니다. 이를 통해 상태 관리 로직을 컴포넌트 바깥에 작성할 수도 있고, 다른 파일에 작성 후 import하여 사용할 수도 있습니다.
useReducer는 두 개의 매개변수를 받습니다.
- reducer 함수
reducer에서 반환하는 상태는 컴포넌트가 가질 새로운 상태가 됩니다.
reducer는 현재 상태(state)와 액션 객체(action)를 파라미터로 받아와서 새로운 상태를 반환해줍니다. action 객체의 형태는 정해진 것이 아니기 때문에 자유롭게 작성하시면 됩니다. 아래 예제에서는 action 객체는 type이라는 속성을 갖고 있습니다. - 초기 상태
useReducer 사용법
const [state, dispatch] = useReducer(reducer, initialState);
- state는 컴포넌트에서 사용하는 상태입니다.
- dispatch는 action 객체에 값을 지정하여 함수를 발생시킵니다.
- initialState는 초기 상태입니다.
import React, { useReducer } from 'react';
const initialState = { count: 0 };
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() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
}
export default Counter;
💁 메모이제이션
메모이제이션은 동일한 계산을 반복하지 않기 위해 이전 계산 결과를 저장하는 기법입니다.
useMemo는 값의 메모이제이션을,
useCallback은 함수의 메모이제이션을 제공합니다.
5. useMemo
useMemo는 메모이제이션된 값을 반환하는 Hook으로, 계산 비용이 높은 연산의 결과를 캐시하여 성능을 최적화합니다.
- 첫 번째 파라미터: 연산할 함수
- 두 번째 파라미터: 의존성 배열
의존성 배열의 내용이 바뀔 때만 등록한 함수를 호출하여 값을 연산해주고, 내용이 바뀌지 않았다면 캐시된 연산된 값을 사용하기 때문에 성능이 최적화됩니다.
import React, { useState, useMemo } from 'react';
function ExpensiveCalculation() {
const [count, setCount] = useState(0);
const [otherState, setOtherState] = useState(0);
const expensiveValue = useMemo(() => {
console.log('Calculating...');
return count * 2;
}, [count]);
return (
<div>
<p>Expensive Value: {expensiveValue}</p>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<button onClick={() => setOtherState(otherState + 1)}>Increment Other State</button>
</div>
);
}
export default ExpensiveCalculation;
6. useCallback
useCallback은 메모이제이션된 콜백을 반환하는 Hook입니다.
useMemo가 값의 메모이제이션이라면 useCallback은 함수의 메모이제이션입니다. 즉, 특정 함수를 새로 만들지 않고 재사용하고 싶을때 사용합니다. 콜백 함수를 생성하는 비용을 줄이고, 종속성 배열의 값이 변경되지 않는 한 동일한 함수 인스턴스를 유지합니다. 앞서 설명한 useMemo와 사용법은 같습니다.
주의할 점은, 함수 안에서 사용하는 state 혹은 props가 있다면, 반드시 의존성 배열안에 해당 값들을 넣어주어야 합니다. 만약 넣어주지 않는다면, 해당 값들을 참조하는 함수가 실행될 때, 가장 최신 값을 참조하지 않을 수 있기 때문입니다.
import React, { useState, useCallback } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount((c) => c + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
export default Counter;
7. useRef
useRef는 DOM 요소에 접근하거나 컴포넌트의 인스턴스 변수처럼 값을 유지하는 데 사용됩니다. 이는 각각의 컴포넌트에 로컬로 저장됩니다.
- useRef는 처음 렌더링 될때 값이 들어가고,
- state 변경 등으로 컴포넌트가 재렌더링이 되어도 해당 컴포넌트가 언마운트될 때까지 값이 그대로 유지됩니다.
- useRef 객체 참조이 변경되어도 렌더링이 발생하지 않습니다. 따라서 컴포넌트 내부 변수들 값이 유지됩니다.
1) useRef 생성
useRef를 생성할 때 초기값을 설정할 수 있습니다. 이렇게 생성된 useRef 객체는 처음에 지정한 초기값으로 설정된 단일 current 속성을 반환합니다.
const 변수명 = useRef(초기값)
{
current: 초기값
}
2) useRef 참조값(current) 변경
useRef의 참조값을 변경해주기 위해서는 current 속성을 수동 직접 변경해주어야 합니다. 아래 예제 처럼 useRef의 참조값을 변경해주어도, 재렌더링이 발생하지 않습니다.
const ref = useRef(0);
const addRef = () => {
refNum.current++;
}
3) useRef로 DOM 요소 접근
useRef를 사용하여 DOM을 조작할 수 있습니다.
아래와 같이 초기값이 null인 ref 객체를 선언하고, ref 객체를 ref 속성으로 조작하려는 DOM 노드의 JSX에 전달해주면 됩니다. return <input ref={inputRef} type="text" />
이렇게 선언하게 되면 React는 useRef 객체의 current 속성을 DOM 노드로 설정합니다. 따라서 current 속성을 통해 DOM 노드에 접근할 수가 있습니다.
아래 예제는, 렌더링 시 DOM 노드 <input>에 포커스를 맞추는 예제입니다.
import React, { useRef, useEffect } from 'react';
function FocusInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} type="text" />;
}
export default FocusInput;
8. useLayoutEffect
useLayoutEffect는 useEffect와 비슷하지만, 모든 DOM 업데이트 후에 동기적으로 발생합니다. 렌더링과 레이아웃 측정을 필요로 하는 작업에 유용합니다.
import React, { useState, useLayoutEffect, useRef } from 'react';
function LayoutEffectComponent() {
const [width, setWidth] = useState(0);
const divRef = useRef(null);
useLayoutEffect(() => {
setWidth(divRef.current.offsetWidth);
}, []);
return (
<div>
<div ref={divRef} style={{ width: '50%' }}>Resize the window</div>
<p>Div width: {width}</p>
</div>
);
}
export default LayoutEffectComponent;
9. useDebugValue
useDebugValue는 React DevTools에서 커스텀 Hook의 디버깅 정보를표시하는 데 사용됩니다.
import React, { useState, useEffect, useDebugValue } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useDebugValue(isOnline ? 'Online' : 'Offline');
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
// 구독 설정 (예시)
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
}, [friendID]);
return isOnline;
}
export default useFriendStatus;
'Front-End > React' 카테고리의 다른 글
React Props와 State를 통한 데이터 관리 (0) | 2024.05.22 |
---|---|
React 리액트에서 UI를 정의할 때 사용하는 JSX: JavaScript XML (0) | 2024.05.22 |
React 기본 구조 완벽 정리, SPA(Single Page Application) 이해하기 (1) | 2024.05.22 |
React 리액트란? 기본 개념, 컴포넌트, 가상 DOM, 설치까지 한눈에 보기 (0) | 2024.05.21 |