🌳 훅(Hook)이란
함수형 컴포넌트에서 리액트의 상태(state)와 생명주기 기능을 연동해주는 함수입니다.
리액트 버전 16.8부터 새롭게 추가된 기능으로, 리액트의 클래스형 컴포넌트에서만 이용할 수 있던 기능을 함수형 컴포넌트에서도 사용할 있게 해줍니다.
따라서 훅을 사용하면 기존의 클래스(class)를 작성하지 않고도 상태와 리액트의 다른 여러 기능들을 이용할 수 있습니다.
훅은 함수형 컴포넌트에 맞게 만들어진 것이므로 함수형 컴포넌트에서만 사용이 가능하며, 'use'라는 키워드로 시작하는 특징을 가지고 있습니다.
리액트에는 여러 가지 훅들이 존재합니다. 이번 글에서는 대표적인 5가지 훅을 정리해보겠습니다.
- useState
- useEffect
- useRef
- useMemo
- useCallback
🌳 대표적인 훅 5가지
1) useState
useState를 사용하면 직접 갱신할 수 있는 상태 변수를 선언할 수 있습니다.
import { useState } from 'react';
export default function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
일반 변수를 선언할 때처럼 어떠한 이름으로도 선언 가능합니다.
여러 개의 상태를 선언해야 한다면 각 상태 변수마다 useState를 호출하여 선언해야 합니다.
useState의 인자로 전달하는 것은 상태의 초기값이며, 반환하는 것은 상태 변수와 해당 변수를 갱신할 수 있는 상태 변화 함수입니다.
구체적인 특징과 사용법은 공식 문서를 참고해주세요.
2) useEffect
useEffect를 이용하여 우리는 리액트에게 해당 컴포넌트가 렌더링 된 이후에 어떤 일을 수행해야 하는지 알려줍니다.
리액트는 우리가 useEffect로 넘긴 함수를 기억했다가 DOM을 업데이트 한 뒤 해당 함수를 불러냅니다.
useEffect는 리액트에 의해 제어되지 않는 외부 시스템과 리액트 컴포넌트를 동기화합니다.
예를 들어, 네트워크, 브라우저 API, 다른 UI 라이브러리와의 연결 등 기타 리액트가 아닌 코드를 다루는 것이 해당됩니다.
따라서 useEffect를 사용하면 부수 효과를 수행할 수 있습니다.
부수 효과 즉, 사이드 이펙트(side effect)란 어떤 함수나 연산의 수행 결과로 시스템의 상태가 예상치 못하게 변경되는 현상입니다.
import { useState, useEffect } from 'react';
export default function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
// 브라우저 API를 이용하여 문서 타이틀을 업데이트
document.title = `You clicked ${count} times`;
}, []);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useEffect는 두 개의 인자를 받습니다.
첫 번째 인자는 컴포넌트가 DOM에 추가된(마운트 된) 이후에 실행할 함수이며, 두 번째 인자는 이 함수에서 참조하는 모든 상태값을 담은 배열입니다. 이 배열은 의존성 배열(dependency array)이라고 하며, 의존성을 생략할 경우 effect는 컴포넌트가 리렌더링 될 때마다 실행됩니다.
useEffect 자체는 값을 반환하지 않으며(undefine 반환), 선택적으로 첫 번째 함수 인자 안에서 정리 함수(clean-up)를 반환할 수 있습니다.
구체적인 특징과 사용법은 공식 문서를 참고해주세요.
3) useRef
useRef는 값 참조와 DOM 조작, 두 가지 방식으로 사용됩니다.
먼저 렌더링 과정에서 화면에 직접 영향을 주지 않는 값을 저장하거나 참조할 때 사용됩니다.
ref는 리렌더링 사이에서도 값을 유지할 수 있으며 ref 내부의 값을 업데이트하려면 current 프로퍼티를 수동으로 변경해야 합니다.
이때 값이 변경되더라도 컴포넌트를 다시 렌더링 하지 않습니다.
또한, 각 컴포넌트에 로컬로 저장되므로 외부와 공유되지 않습니다.
import { useRef } from 'react';
export default function Counter() {
let ref = useRef(0);
function handleClick() {
ref.current = ref.current + 1;
alert('You clicked ' + ref.current + ' times!');
}
return (
<button onClick={handleClick}>
Click me!
</button>
);
}
다음으로 ref를 DOM 노드에 연결하면 해당 요소에 직접 접근하여 조작할 수 있습니다.
초기값을 null로 설정한 ref 객체를 생성해 조작하려는 DOM 노드의 JSX에 전달하면 됩니다.
아래 예시는 버튼을 클릭하면 입력창에 포커스가 맞춰지는 코드입니다.
import { useRef } from 'react';
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
useRef는 current라는 하나의 프로퍼티만을 가진 객체( { current: '초기값' } )로, 초기값을 인자로 받습니다.
그리고 해당 객체를 반환합니다.
구체적인 특징과 사용법은 공식 문서를 참고해주세요.
4) useMemo
useMemo를 사용하면 재렌더링 사이에 비용이 많이 드는 계산 결과(값)를 캐싱할 수 있습니다.
useMemo는 함수를 호출하고 그 결과를 캐싱합니다.
이렇게 반환값을 캐싱하는 것을 메모제이션(memoization)이라고 합니다.
import { useState, useMemo } from "react";
export default function Example() {
const [count, setCount] = useState(0);
// 시간이 오래 걸리는 함수 (ex - 무거운 계산)
const expensiveCalculation = (num) => {
// ...복잡한 계산 로직...
return result;
};
const computedValue = useMemo(() => expensiveCalculation(count), [count]);
return (
<div>
<h1>계산 결과: {computedValue}</h1>
<button onClick={() => setCount(count + 1)}>카운트 증가</button>
</div>
);
}
useMemo는 두 개의 인자를 받습니다.
첫 번째 인자는 계산 함수로, 인자를 필요로 하지 않고 순수하게 어떤 값만을 반환하는 함수여야 합니다.
두 번째 인자는 의존성 배열로, 계산 함수 내에서 참조하는 모든 상태값이나 변수를 포함합니다.
리액트는 초기 렌더링 시 계산 함수를 실행해 얻은 결괏값을 저장하고, 의존성 배열에 포함된 값들이 변경되지 않았다면 후속 렌더링 시에는 저장해 둔 결괏값을 재사용합니다.
따라서 useMemo는 계산된 값을 저장해 불필요한 재계산을 줄이고 성능 최적화에 도움을 줍니다.
구체적인 특징과 사용법은 공식 문서를 참고해주세요.
5) useCallback
useCallback을 사용하면 리렌더링 간에 함수 정의를 캐싱할 수 있습니다.
useCallback은 함수 자체를 캐싱합니다.
useMemo가 값을 메모이제이션하듯, useCallback도 전달한 함수를 메모이제이션해 동일한 참조를 유지합니다.
import { useCallback } from "react";
export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
return (
<div className={theme}>
{/* ShippingForm은 같은 props를 받게 되고, 리렌더링을 건너뛸 수 있음 */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
useCallback는 두 개의 인자를 받습니다.
첫 번째 인자는 캐싱할 함수로, 어떠한 인자나 반환값을 가질 수 있습니다.
두 번째 인자는 의존성 배열로, 함수 내에서 참조하는 모든 상태값이나 변수를 포함합니다.
리액트는 최초 렌더링에서 캐싱해 둔 함수를 반환하며(not 호출), 의존성 배열에 포함된 값들이 이전과 같다면 후속 렌더링 시에는 같은 함수를 그대로 반환합니다.
따라서 useCallback은 함수를 저장해 불필요한 재생성을 줄이고 useMemo와 같이 성능 최적화에 도움을 줍니다.
구체적인 특징과 사용법은 공식 문서를 참고해주세요.
🌳 훅 사용 규칙
훅을 호출할 때는 두 가지 규칙을 지켜야 합니다.
1) 최상위에서만 호출하기
훅은 컴포넌트 함수의 최상위(at the top level)에서만 호출해야 합니다.
여기서 최상위란 함수 컴포넌트 본문에서 가장 바깥 레벨을 의미하며, 반복문, 조건문, 함수 내부, early return 같은 흐름 제어 구문 안에서는 훅을 호출할 수 없습니다.
이 규칙을 따르면 컴포넌트가 렌더링 될 때마다 훅이 항상 같은 순서로 호출되는 것이 보장됩니다.
리액트는 훅을 호출한 순서로 상태를 기억하기 때문에, 조건문이나 early return 내부에서 훅을 호출하면 렌더링마다 호출 순서가 달라져 상태가 잘못 연결될 수 있습니다.
2) 리액트 함수 내에서만 호출하기
훅은 리액트 함수 컴포넌트나 커스텀 훅 안에서만 호출해야 합니다.
일반적인 자바스크립트 함수(ex-이벤트 핸들러, 유틸 함수, 클래스 메서드 등) 안에서 호출하게 되면 렌더링 주기와 훅 호출이 연동되지 않아 리액트가 상태를 제대로 추적할 수 없습니다.
더 자세한 내용은 공식 문서를 참고해주세요.
읽어주셔서 감사합니다:)