Hook은 React 16.8부터 새로 추가된 기능으로, 기존의 클래스 기반 코드 없이도 상태 값 관리와 여러 React 기능을 사용할 수 있게 해주는 함수이다.
Hook을 사용하면 컴포넌트에서 다양한 React 기능을 활용할 수 있으며, 내장된 Hook을 이용하거나 커스텀 Hook을 만들어 사용할 수 있다.
메모제이션이란, 컴포넌트가 불필요한 리렌더링을 막아 성능을 최적화하는 기법이다.
React.memo
: 컴포넌트의 불필요한 리렌더링을 방지해주는 함수이다.
useCallback
: 함수를 메모이제이션하여, 동일한 함수가 반복해서 생성되지 않도록 방지해주는 Hook이다.
useMemo
: 복잡한 연산의 결과값을 메모이제이션하여, 재계산을 방지해주는 Hook이다.
React.memo는 고차 컴포넌트로, 컴포넌트의 props가 변경되지 않으면 컴포넌트를 리렌더링하지 않도록 해준다.
고차 컴포넌트(Higher-Order Component, HOC)란?
-> 컴포넌트를 인자로 받아서 새로운 컴포넌트를 반환하는 함수
const EnhancedComponent = higherOrderComponent(WrappedComponent);
import React, { useState } from 'react';
// React.memo로 최적화된 컴포넌트
const MyComponent = React.memo(({ name }) => {
console.log("렌더링");
return <div>{name}</div>;
});
function App() {
const [name, setName] = useState('Alice');
return (
<div>
<MyComponent name={name} />
<button onClick={() => setName('Bob')}>이름 변경</button>
</div>
);
}
export default App;
React.memo
는 props가 변경되지 않으면 해당 컴포넌트를 리렌더링하지 않도록 최적화한다.
이 예시에서는 name
이 Alice
에서 Bob
으로 변경될 때만 MyComponent
가 리렌더링된다.
App
컴포넌트에서 setName
을 통해 name
이 변경되면,MyComponent
가 리렌더링되지만, 그 외의 상태 변경(예: App
에서 name
과 관련 없는 다른 상태 변경)은 MyComponent에
영향을 미치지 않으므로 MyComponent
는 리렌더링되지 않는다.
useCallback은 함수가 불필요하게 재생성(재정의)되는 것을 방지하는 훅이다.
주로 함수를 props로 전달할 때 사용한다.
function ParentComponent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<ChildComponent onClick={handleClick} />
</div>
);
}
useCallback을 사용하여 handleClick
의 참조값을 메모제이션 했기 때문에, ParentComponent
가 리렌더링되어도 함수가 재생성되지 않는다.
+) 이 코드에서는 의존성 배열이 비어있으므로, handleClick
함수는 한번만 생성되고 그 이후로는 동일한 함수 참조를 계속 사용한다.
useMemo는 계산량이 많거나 복잡한 연산의 결과를 캐싱해서 성능을 최적화(불필요한 재계산 방지) 할 때 사용한다.
import React, { useState, useMemo } from 'react';
function App() {
const [count, setCount] = useState(0);
// 복잡한 계산 (예시: 제곱 계산)
const squared = useMemo(() => {
console.log("계산 중...");
return count * count;
}, [count]);
return (
<div>
<p>Count: {count}</p>
<p>Squared: {squared}</p>
<button onClick={() => setCount(count + 1)}>Increase Count</button>
</div>
);
}
export default App;
useMemo
를 사용하여 squared
값을 계산한다. count
값이 변경될 때만 count * count
계산이 다시 실행된다.
import React, { useState, useMemo } from 'react';
function App() {
const [count, setCount] = useState(0);
// 객체 생성 (useMemo를 사용하여 객체를 메모이제이션)
const objectValue = useMemo(() => {
return { count }; // { count: count }와 동일(객체리터럴)
}, [count]);
return (
<div>
<p>Count: {count}</p>
<p>Object: {JSON.stringify(objectValue)}</p>
<button onClick={() => setCount(count + 1)}>Increase Count</button>
</div>
);
}
export default App;
objectValue
는 { count: 0 }
과 같이 { count: 숫자 }
형태의 객체이다. useMemo는 이 객체를 메모이제이션하여 count 값이 변경되지 않는 한 이전 객체를 재사용한다.
메모이제이션은 유용하지만, 메모이제이션도 메모리를 사용하기 때문에 필요 이상으로 남용하면 성능을 오히려 떨어뜨릴 수 있다.
useRef는 컴포넌트의 상태가 변경되더라도 리렌더링을 유발하지 않고 값을 저장하는 데 유용한 훅이다.
import React, { useRef } from 'react';
function FocusInput() {
const inputRef = useRef(null); // input 요소를 참조하는 useRef 생성
const handleFocus = () => {
// input 요소에 포커스 설정
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" placeholder="버튼을 클릭하면 포커스가 이동합니다." />
<button onClick={handleFocus}>포커스 이동</button>
</div>
);
}
export default FocusInput;
여기서 current
란 useRef
로 만든 객체의 속성 중 하나다.
왜 inputRef.focus() 가 아니라 inputRef.current.focus()일까?
-> useRef 객체를 반환하기 때문이다.
inputRef는 아래와 같은 형태로 초기화되는 것이다.{ current: null // 초기값은 null }
즉, inputRef는 dom요소를 직접 가리키고 있는 것이 아니고 inputRef 객체의 current 속성 안에 해당 dom요소가 할당되는것이다.
처음에는 null로 시작하고 있고, DOM요소가 렌더링되면 그 요소가 current에 할당되고 있다. 버튼을 클릭하면 handleFocus
함수에서 inputRef.current.focus()
를 호출하여 input 요소에 포커스를 설정한다.
이렇게 useRef를 사용하면, DOM 요소에 직접 접근하여 상태를 변경할 수 있고, ref로 참조된 DOM 요소는 컴포넌트가 리렌더링되어도 리렌더링되지 않는다.
useRef는 리렌더링을 발생시키지 않고 데이터를 저장할 수 있기 때문에, 렌더링에 영향을 미치지 않아야 하는 데이터를 저장하는 데 유용하다.
import React, { useState, useEffect, useRef } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef(count);
useEffect(() => {
// 컴포넌트가 업데이트될 때마다 이전 카운터 값을 저장
prevCountRef.current = count;
}, [count]);
return (
<div>
<p>현재 값: {count}</p>
<p>이전 값: {prevCountRef.current}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}
export default Counter;
prevCountRef는 useRef로 초기화되며, 초기값은 count 값이다.
useEffect는 count 값이 변경될 때마다 실행되고, 현재 count값을 저장하게 된다.
useEffect는 렌더링 후에 실행되므로, 항상 prevCountRef.current에는 이전 렌더링에서의 count 값이 담기게 된다.
왜
<p>이전 값: {prevCountRef.current}</p>
는 계속 이전 값을 담게 될까?
이유는, useEffcet의 실행 시점 때문. useEffect는 JSX가 렌더링이 완료되었을 시점에서 실행된다. 즉,<p>이전 값: {prevCountRef.current}</p>
가 먼저 그려진 후에 useEffect가 실행되기에 아직 prevCountRef.current로 보여지는 값은 리렌더링 이전의 값일 것이다.
이외에도, setInterval과 setTimeout 제어, 컴포넌트의 마운트 여부 추적, 애니메이션 효과 제어 등의 사용법이 있으나 현재 여기까지 이해하고 정리하는데도 어려웠기 때문에... 실제로 사용 시 다시 정리하고자 한다. 😔😔
훅은 렌더링이 시작되기 전에 선언되어야 한다. 렌더링 이후에 훅이 호출되면 리액트가 컴포넌트의 상태와 생명주기를 올바르게 관리 할 수 없다. 이로 인해 오류가 나거나 불필요한 리렌더링이 발생할 수 있으니 주의.
import React, { useState, useEffect } from 'react';
function ExampleComponent({ show }) {
// 조건에 따라 훅을 나중에 선언하는 잘못된 패턴
if (show) {
const [count, setCount] = useState(0); // 이 훅은 조건에 따라 실행됨
}
useEffect(() => {
console.log("컴포넌트가 업데이트되었습니다.");
});
return <div>컴포넌트</div>;
}
export default ExampleComponent;
이 코드에서는 show가 true일 때만 useState가 호출된다. 그러나 리액트는 컴포넌트가 처음 렌더링될 때 모든 훅이 같은 순서로 호출될 것을 기대한다. 훅의 위치가 바뀌면 리액트는 상태를 제대로 관리하지 못해 오류가 발생하거나 예기치 않은 동작이 일어날 수 있다.
import React, { useState, useEffect } from 'react';
function ExampleComponent({ show }) {
// 최상위에서 훅을 선언
const [count, setCount] = useState(0);
useEffect(() => {
console.log("컴포넌트가 업데이트되었습니다.");
});
return (
<div>
컴포넌트
{show && <p>Count: {count}</p>}
</div>
);
}
export default ExampleComponent;
따라서 조건문이나 반복문을 사용해 훅의 위치가 바뀌지 않도록 주의해야 한다!
function someUtilityFunction() {
const [value, setValue] = useState(0); // 규칙 위반
return value;
}
일반 함수 내부에서 useState를 호출하는 것은 규칙 위반이니 주의.
React에서는 훅을 남용할 경우 성능이 저하되거나 예상치 못한 문제가 발생할 수 있다. 예를 들어, 상태가 아닌 값을 저장하기 위해 useState
를 사용하는 것은 불필요하다. 리렌더링이 필요하지 않은 데이터는 useRef
를 사용하는 것이 더 적절하고, 때로는 let
변수로도 충분하다.
useEffect와 useState 하나의 컴포넌트 내에서 너무 많이 쓰인다면, 컴포넌트 분리하여 로직을 단순화 시키는 것을 고려해야한다.