1. 훅이란 무엇인가?
기존 함수 컴포넌트는 클래스 컴포넌트와 다르게 코드도 굉장히 간결하고, 별도로 state를 정의해서 사용하거나 컴포넌트의 생명주기에 맞춰 어떤 코드가 실행되도록 할 수 없었습니다. 따라서 함수 컴포넌트에 이런 기능을 지원하기 위해서 나온 것이 바로 훅입니다. 훅을 사용하면 함수 컴포넌트도 클래스 컴포넌트의 기능을 모두 동일하게 구현할 수 있게 되는 것이죠.
Hook이라는 영단어는 보통 프로그래밍에서 원래 존재하는 어떤 기능에 마치 갈고리를 거는 것처럼 끼어 들어가 같이 수행되는 것을 의미합니다. 리액트 훅도 마찬가지로 리액트의 state와 생명주기 기능에 갈고리를 걸어 원하는 시점에 정해진 함수를 실행되도록 만든 것입니다.
훅의 이름은 모두 use로 시작합니다. 개발자가 직접 커스텀 훅을 만들어서 사용할 수도 있는데 커스텀 훅은 개발자 마음대로 이름을 지을 수 있지만 use를 이름 앞에 붙여 훅이라는 것을 나타내주는 것이 좋습니다.
2. useState
가장 대표적이고 많이 사용되는 훅으로 state를 사용하기 위한 훅입니다.
import React, { useState } from "react";
function Counter(props) {
const [count, setCount] = useState(0);
return (
<div>
<p>총 {count}번 클릭했습니다.</p>
<button onClick = {() => setCount(count + 1)}>클릭</button>
</div>
);
}
위 코드는 useState()를 사용하여 카운트 값을 state로 관리하도록 만든 것입니다. 버튼이 눌렸을 때 setCount() 함수를 호출해서 카운트를 1 증가시킵니다. 그리고 count의 값이 변경되면 컴포넌트가 리렌더링되면서 화면에 새로운 카운트 값이 표시됩니다.
3. useEffect
useEffect()는 사이드 이펙트를 수행하기 위한 훅입니다. 사이드 이펙트란 리액트에서 부작용이 아니라 효과, 영향을 뜻하는 이펙트의 의미에 가깝습니다. 예를 들면 서버에서 데이터를 받아오거나 수동으로 DOM을 변경하는 등의 작업을 의미합니다. 이런 작업을 이펙트라고 부르는 이유는 이 작업들이 다른 컴포넌트에 영향을 미칠 수 있으며 렌더링 중에는 작업이 완료될 수 없기 때문입니다.
useEffect()는 클래스 컴포넌트에서 제공하는 생명주기 함수인 componentDidMount(), componentDidUpdate(), componentWillUnmount()와 동일한 기능을 하나로 통합해서 제공합니다. useEffect()는 아래와 같이 사용합니다.
useEffect(이펙트 함수, 의존성 배열);
첫 번째 파라미터로는 이펙트 동작을 정의하는 함수가 들어가고, 두 번째 파라미터로는 의존성 배열이 들어갑니다. 의존성 배열은 말 그대로 이 이펙트가 의존하고 있는 배열인데 배열 안에 있는 변수 중에 하나라도 값이 변경되었을 때 이펙트 함수가 실행됩니다. 기본적으로 이펙트 함수는 처음 컴포넌트가 렌더링된 이후와 업데이트로 인한 리렌더링 이후에 실행됩니다.
만약 이펙트 함수가 마운트와 언마운트시에 단 한번씩만 실행되게 하고 싶으면, 의존성 배열에 빈 배열을 넣으면 됩니다. 이렇게 하면 해당 이펙트가 porps나 state에 있는 어떤 값에도 의존하지 않는 것이 되므로 여러 번 실행되지 않습니다.
의존성 배열은 생략할 수도 있는데 이 경우에는 컴포넌트가 업데이트될 때마다 호출됩니다.
import React, { useState, useEffect } from "react";
function Counter(props) {
const [count, setCount] = useState(0);
// componentDidMount, componentDidUpdate와 비슷하게 작동합니다.
useEffect(()=>{
document.title = `총 ${count}번 클릭했습니다.`;
});
return (
<div>
<p>총 {count}번 클릭했습니다.</p>
<button onClick = {() => setCount(count + 1)}>클릭</button>
</div>
);
}
이펙트는 함수 컴포넌트 안에서 선언되기 때문에 해당 컴포넌트의 props와 state에 접근할 수도 있습니다. 위 코드에서는 count라는 state에 접근하여 해당 값이 포함된 문자열을 생성해서 사용하는 것을 볼 수 있습니다.
그렇다면 componentWillUnmount()와 동일한 기능은 useEffect()로 어떻게 구현할 수 있을까요?
import React, { useState, useEffect } from "react";
function UserStatus(props) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ServerAPI.subscribeuserStatus(props.user.id, handleStatusChange);
return () => {
ServerAPI.unsubscribeUserStatus(props.user.id, handleStatusChange);
};
});
if (isOnline = null) {
return '대기중...';
}
return isOnline ? '온라인' : '오프라인';
}
위 코드는 useEffect()에서 먼저 ServerAPI를 사용하여 사용자의 상태를 구독하고 있습니다. 이후 함수 하나를 리턴하는데 해당 함수 안에는 구독을 해지하는 API를 호출하도록 되어있습니다. useEffect()에서 리턴하는 함수는 컴포넌트가 마운트 해제될 때 호출됩니다. 결과적으로 useEffect()의 리턴 함수의 역할은 componentWillUnmount() 함수가 하는 역할과 동일합니다.
또한 useEffect() 훅은 하나의 컴포넌트에 여러 개를 사용할 수 있습니다.
useEffect(() => {
// 컴포넌트가 마운트된 이후,
// 의존성 배열에 있는 변수들 중 하나라도 값이 변경되었을 때 실행됨
// 의존성 배열에 빈 배열([])을 넣으면 마운트와 언마운트시에 단 한 번씩만 실행됨
// 의존성 배열 생략 시 컴포넌트 업데이트 시마다 실행됨
return () => {
// 컴포넌트가 마운트 해제되기 전에 실행됨
};
}, [의존성 변수1, 의존성 변수2, ...]);
4. useMemo
메모이제이션은 비용이 높은(연산량이 많이 드는) 함수의 호출 결과를 저장해 두었다가, 같은 입력값으로 함수를 호출하면 새로 함수를 호출하지 않고 이전에 저장해놨던 호출 결과를 반환하는 것입니다. 이렇게 하면 결과적으로 함수 호출 결과를 받기까지 걸리는 시간도 짧아질뿐더러 불필요한 중복 연산도 하지 않기 때문에 컴퓨터의 자원을 적게 쓰게 됩니다. 이 때 저장된 결과 값들을 Memoized value라고 합니다.
useMemo() 훅은 Memoized value를 리턴하는 훅입니다. useMemo() 훅은 파라미터로 Memoized value를 생성하는 create 함수와 의존성 배열을 받습니다. 의존성 배열에 들어있는 변수가 변했을 경우에만 create 함수를 호출하여 결괏값을 반환하며, 그렇지 않은 경우에는 기존 함수의 결괏값을 그대로 반환합니다. useMemo() 훅을 사용하면 컴포넌트가 다시 렌더링될 때마다 연산량이 높은 작업을 반복하는 것을 피할 수 있고 결과적으로 빠른 렌더링 속도를 얻을 수 있습니다.
const memoizedValue = useMemo(
() => {
// 연산량이 높은 작업을 수행하여 결과를 반환
return computeExpensiveValue(의존성 변수1, 의존성 변수2);
},
[의존성 변수1, 의존성 변수2]
);
useMemo() 훅을 사용할 때 기억해야 할 점은 useMemo()로 전달된 함수는 렌더링이 일어나는 동안 실행된다는 점입니다. 그렇기 때문에 일반적으로 렌더링이 일어나는 동안 실행돼서는 안될 작업을 useMemo()의 함수에 넣으면 안됩니다. 예를 들면 useEffect() 훅에서 실행돼야 할 사이드 이펙트 같은 것이 있습니다. 서버에서 데이터를 받아오거나 수동으로 DOM을 변경하는 작업 등은 렌더링이 일어나는 동안 실행돼서는 안되기 때문에 useMemo() 훅의 함수에 넣으면 안되고 useEffect() 훅을 사용해야 합니다.
만약 의존성 배열을 넣지 않을 경우 렌더링이 일어날 때마다 매번 함수가 실행되기 때문에 아무런 의미가 없습니다.
만약 의존성 배열로 빈 배열을 넣게 되면 컴포넌트 마운트 시에만 함수가 실행됩니다.
useMemo()의 create 함수에서 참조하는 모든 변수를 의존성 배열에 넣어주는 것이 본래 useMemo()의 의미에 맞습니다. 이를 위해 eslint-plugin-react-hooks 패키지를 사용하면 도움이 됩니다. 이 패키지는 의존성 배열이 잘못 되어있는 경우에 자동으로 경로 표시를 해주며 고칠 방법을 재안해 줍니다.
5. useCallback
useCallback() 훅은 useMemo() 훅과 유사한 역할을 하는데 차이점은 값이 아닌 함수를 반환한다는 점입니다. useCallback() 훅에서는 파라미터로 받는 함수를 콜백이라고 부릅니다. 그리고 의존성 배열에 있는 변수 중 하나라도 변경되면 Memoized 콜백 함수를 반환합니다.
const memoizedCallback = useCallback(
() => {
doSomething(의존성 변수1, 의존성 변수2);
},
[의존성 변수1, 의존성 변수2]
);
만약 useCallback() 훅을 사용하지 않고 컴포넌트 내에 함수를 정의한다면 매번 렌더링이 일어날 때마다 함수가 새로 정의됩니다. 따라서 useCallback() 훅을 사용하여 특정 변수의 값이 변한 경우에만 함수를 다시 정의하도록 해서 불필요한 작업을 없애주는 것입니다.
예를 들어 useCallback() 훅을 사용하지 않고 컴포넌트 내에서 정의한 함수를 자식 컴포넌트에 props로 넘겨 사용하는 경우, 부모 컴포넌트가 리렌더링 될 때마다 매번 자식 컴포넌트도 리렌더링됩니다. 하지만 useCallback() 훅을 사용하면 특정 변수의 값이 변한 경우에만 함수를 다시 정의하게 되므로, 함수가 다시 정의되지 않는 경우에는 자식 컴포넌트의 리렌더링이 일어나지 않습니다.
6. useRef
useRef() 훅은 레퍼런스를 사용하기 위한 훅입니다. 리액트에서 레퍼런스란 특정 컴포넌트에 접근할 수 있는 객체를 의미합니다. useRef() 훅은 레퍼런스 객체를 반환하고, .current라는 속성으로 현재 레퍼런스하고 있는 엘리먼트에 접근합니다.
const refContainer = useRef(초깃값);
위와 같이 useRef() 훅을 사용하면 파라미터로 들어온 초깃값으로 초기화된 레퍼런스 객체를 반환합니다. 이렇게 반환된 레퍼런스 객체는 컴포넌트의 라이프타임 전체에 걸쳐서 유지됩니다. 즉, 컴포넌트가 마운트 해제되기 전까지는 계속 유지된다는 것이죠. 쉽게 말해 useRef() 훅은 변경 가능한 .current라는 속성을 가진 하나의 상자라고 생각하면 됩니다.
function TextInputWithFocusButton(props) {
const inputElem = useRef(null);
const onButtonClick = () => {
// 'current'는 마운트된 input element를 가리킴
inputElem.current.focus();
};
return (
<>
<input ref={inputElem} type="text" />
<button onClickc={onButtonClick}>Focus the input</button>
</>
);
}
위 코드는 useRef() 훅을 사용하여 버튼 클릭 시 <input>에 포커스를 하도록 하는 코드입니다. 초깃값으로 null을 넣었고 결과로 반환된 inputElem이라는 레퍼런스 객체를 <input>태그에 넣어줬습니다. 그리고 버튼 클릭 시 호출되는 함수에서 inputElem.current를 통해 실제 엘리먼트에 접근하여 focus() 함수를 호출하고 있습니다.
HTML에서 ref 속성을 통해 DOM에 접근할 수 있습니다. 비슷하게 리액트에서 <div ref={myRef} />라는 코드를 작성하면 node가 변경될 때마다 myRef의 .current 속성에 현재 해당되는 DOM node를 저장합니다.
ref 속성과 기능은 비슷하지만 useRef() 훅은 클래스의 인스턴스 필드를 사용하는 것과 유사하게 다양한 변수를 저장할 수 있다는 장점이 있습니다. 이게 가능한 이유는 useRef() 훅이 일반적인 자바스크립트 객체를 리턴하기 때문입니다. 또한 useRef() 훅은 매번 렌더링될 때마다 항상 같은 ref 객체를 반환합니다.
한 가지 기억해야 할 점은 useRef() 훅은 내부의 데이터가 변경되었을 때 별도로 알리지 않는다는 점입니다. .current 속성을 변경하는 것은 리렌더링을 일으키지 않습니다. 따라서 ref에 DOM node가 연결되거나 분리되었을 경우 어떤 코드를 실행하고 싶다면 callback ref를 사용해야 합니다.
DOM node의 변화를 알기 위해 callback ref를 사용할 수 있습니다. 리액트는 ref가 다른 node에 연결될 때마다 콜백을 호출하게 됩니다.
functio9n MeasureExample(props) {
const[height, setHeight] = useState(0);
const measureRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<>
<h1 ref={measuredRef}>안녕, 리액트</h1>
<h2>위 헤더의 높이는 {Math.round(height)}px 입니다.<h2/>
</>
);
}
위 코드에는 useRef() 훅을 사용하지 않고 useCallback() 훅을 이용한 callback ref 방식을 사용했습니다. callback ref 방식을 사용하게 되면 자식 컴포넌트가 변경되었을 때 알림을 받을 수 있고, 이를 통해 다른 정보들을 업데이트할 수 있습니다. 이 예제 코드에서는 <h1> 태그의 높이 값을 매번 업데이트하고 있습니다.
7. 훅의 규칙
훅을 사용할 때 지켜야할 두 가지 규칙이 있습니다. 첫 번쨰 규칙은 훅은 무조건 최상위 레벨에서만 호출해야 한다는 것입니다. 여기에서 말하는 최상위 레벨은 리액트 함수 컴포넌트의 최상위 레벨을 의미합니다. 따라서 반복문이나 조건문 또는 중첩된 함수들 안에서 훅을 호출하면 안된다는 뜻입니다. 이 규칙에 따라서 훅은 컴포넌트가 렌더링될 때마다 매번 같은 순서로 호출되어야 합니다. 그렇게 해야 리액트가 다수의 useState() 훅과 useEffect() 훅의 호출에서 컴포넌트의 state를 올바르게 관리할 수 있게 됩니다.
function MyComponent(props) {
const [name, setName] = useState('Inje');
if (name !== '') {
useEffect(() => {
...
});
}
...
}
위 코드에서는 name !== '' 라는 조건문의 값이 참인 경우에만 useEffect() 훅을 호출하도록 되어 있습니다. 이런 경우 중간에 name 값이 빈 문자열이 되면 useEffect() 훅이 호출되지 않습니다. 결과적으로 렌더링할 때마다 훅이 같은 순서대로 호출되는 것이 아니라 조건문의 결과에 따라 호출되는 훅이 달라지므로 잘못된 코드입니다.
두 번째 규칙은 리액트 함수 컴포넌트에서만 훅을 호출해야 한다는 것입니다. 그렇기 때문에 일반적인 자바스크립트 함수에서 훅을 호출하면 안됩니다. 훅은 리액트 함수 컴포넌트에서 호출하거나 직접 만든 커스텀 훅에서만 호출할 수 있습니다. 이 규칙에 따라 리액트 컴포넌트에 있는 state와 관련된 모든 로직은 소스코드를 통해 명확하게 확인이 가능해야 합니다.
8. 나만의 훅 만들기
커스텀 훅은 기본적으로 제공되는 훅들 이외에 추가적으로 필요한 기능을 모아놓은 것입니다. 커스텀 훅을 만드는 이유는 여러 컴포넌트에서 반복적으로 사용되는 로직을 훅으로 만들어 재사용하기 위함입니다.
커스텀 훅을 만들어야 하는 상황
아래 코드의 UserStatus 컴포넌트는 isOnline이라는 state에 따라서 사용자의 상태가 온라인인지 아닌지를 텍스트로 보여주는 컴포넌트입니다.
import React, { useState, useEffect } from 'react';
function UserStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ServerAPI.subscribeUserStatus(props.user.id, handleStatusChange);
return () => {
ServerAPI.unsubscribeUserStatus(props.user.id, handleStatusChange);
};
});
if (isOnline = null) {
return '대기중...';
}
return isOnline ? '온라인' : '오프라인';
}
그리고 동일한 웹사이트에서 연락처 목록을 제공하는데 이때 온라인인 사용자의 이름은 초록색으로 표시해주고 싶다고 가정하고, 이 컴포넌트의 이름을 UserListItem이라고 합시다. 여기에는 위 코드와 비슷한 로직이 들어갑니다.
import React, { useState, useEffect } from 'react';
function UserListItem(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ServerAPI.subscribeUserStatus(props.user.id, handleStatusChange);
return () => {
ServerAPI.unsubscribeUserStatus(props.user.id, handleStatusChange);
};
});
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.user.name}
</li>
);
}
코드를 살펴보면 위에 나온 UserStatus와 useState(), useEffect() 훅을 사용하는 부분이 동일한 것을 알 수 있습니다. 여러 곳에서 중복되는 코드인 것이죠. 이런 경우 중복되는 코드를 추출하여 커스텀 훅으로 만들 수 있습니다.
커스텀 훅 추출하기
두 개의 자바스크립트 함수에서 하나의 로직을 공유하도록 하고 싶을 때는 새로운 함수를 하나 만드는 방법을 사용합니다. 커스텀 훅은 무언가 특별한 것이 아니라 이름이 use로 시작하고 내부에서 다른 훅을 호출하는 하나의 자바스크립트 함수입니다. 아래 예제 코드를 봅시다.
import { useState, useEffect } from 'react';
function useUserStatus(userId) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ServerAPI.subscribeUserStatus(userId, handleStatusChange);
return () => {
ServerAPI.unsubscribeUserStatus(userId, handleStatusChange);
};
});
return isOnline;
}
위 코드를 보면 특별한 것이 없고 UserStatus, UserListItem 두 컴포넌트에서 중복되는 로직을 추출해서 가져왔습니다. 다만 다른 컴포넌트 내부에서와 마찬가지로 훅을 호출하는 것은 무조건 커스텀 훅의 최상위 레벨에서만 해야 합니다.
커스텀 훅 사용하기
이제 중복되는 로직을 useUserStatus() 훅을 사용하여 다음과 같이 코드를 변경할 수 있습니다.
import React from 'react';
import useUserStatus from './useUserStatus';
function UserStatus(props) {
const isOnline = useUserStatus(props.user.id);
if (isOnline = null) {
return '대기중...';
}
return isOnline ? '온라인' : '오프라인';
}
function UserListItem(props) {
const isOnline = useUserStatus(props.user.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.user.name}
</li>
);
}
커스텀 훅의 이름은 꼭 use로 시작해야 한다는 점을 기억해주시기 바랍니다. 만약 이름이 use로 시작하지 않는다면 특정 함수의 내부에서 훅을 호출하는지를 알 수 없기 때문에 훅의 규칙 위반 여부를 자동으로 확인할 수 없습니다.
커스텀 훅은 단순히 state와 연관된 로직을 재사용이 가능하게 만든 것이기 때문에 여러 개의 컴포넌트에서 하나의 커스텀 훅을 사용할 때에 컴포넌트 내부에 있는 모든 state와 effects는 전부 분리되어 있습니다.
훅들 사이에서 데이터를 공유하는 방법
const userList = [
{id: 1, name: 'Inje'},
{id: 2, name: 'Mike'},
{id: 3, name: 'Steve'},
];
function ChagUserSelector(props) {
const [userId, setUserId] = useState(1);
const isUserOnline = useUserStatus(userId);
return (
<>
<Circle color={isUserOnline ? 'green' : 'rde'} />
<select
value={userId}
onChange={event => setUserId(Number(event.target.value))}
>
{userList.map(user => (
<option key={user.id} value={user.id}>
{user.name}
</option>
))}
</select>
</>
);
}
위 코드를 보면 useState() 훅을 사용해서 userId라는 state를 만들었습니다. 현재 선택된 사용자의 아이디를 저장하기 위한 용도이죠. 그리고 이 userId는 바로 다음에 나오는 useUserStatus 훅의 파라미터로 들어가게 됩니다. 이렇게 하면 setUserId 함수를 통해 userId가 변경될 때마다, useUserStatus 훅은 이전에 선택된 사용자를 구독 취소하고 새로 선택된 사용자의 온라인 여부를 구독하게 됩니다. 훅들 사이에서는 이러한 방법으로 데이터를 공유할 수 있습니다.
'개발 공부' 카테고리의 다른 글
| [Front-end] React 조건부 렌더링 (0) | 2023.09.30 |
|---|---|
| [Front-end] React 이벤트 핸들링 (1) | 2023.09.30 |
| [Java 8] Stream API 기본 문법 (0) | 2023.09.28 |
| [Front-end] React State와 생명주기 (1) | 2023.09.26 |
| [Front-end] React 컴포넌트와 Props (0) | 2023.09.26 |