Hooks
Hook이란
Hook은 리액트가 처음 나왔을 때부터 있던 개념은 아니고 리액트 v16.8에서 새롭게 등장한 개념이다.
리액트로 개발을 할 때 대부분 훅을 사용하기 때문에 훅에 대해 잘 이해하는 것이 중요하다.
앞서 학습 글에서 리액트 컴포넌트는 두 가지 종류가 있다고 했었다.
하나는 Class Component 다른 하나는 Function Component 였다.
또한 컴포넌트에는 state라는 중요한 개념이 등장했었는데, 이 state를 이용해서 렌더링에 필요한 데이터를 관리했었다.
클래스 컴포넌트에서는 생성자, Constructor에서 이 state를 정의하고 setState() 함수를 통해 state를 업데이트 하였다.
이처럼 클래스 컴포넌트는 state와 관련된 기능뿐만 아니라 컴포넌트의 생명주기 함수들까지 모두 명확하게 정의 되어 있기 때문에 잘 가져다 쓰기만 하면 된다.
하지만 기존 함수 컴포넌트는 클래스 컴포넌트와는 다르게 코드도 굉장히 간결하고 별도로 state를 정의해서 사용하거나 컴포넌트의 생명주기에 맞춰 어떤 코드가 실행되도록 할 수 없었다.
따라서 함수 컴포넌트에 이런 기능을 지원하기 위해서 나온 것이 바로 Hooks이다.
훅을 사용하면 함수 컴포넌트도 클래스 컴포넌트의 기능을 모두 동일하게 구현할 수 있게 되는 것이다.
hook이라는 영어 단어는 갈고리라는 뜻을 가지고 있는데 보통 프로그래밍에서는 원래 존재하는 어떤 기능에 마치 갈고리를 거는 것처럼 끼어 들어가, 같이 수행되는 것을 의미한다.
비슷하게 자주 사용되는 용어로는 webHook이라는 것이 있다.
리액트의 훅도 마찬가지로 리액트의 state와 생명주기 기능에 갈고리를 걸어 원하는 시점에 정해진 함수가 실행 되도록 만든 것이며, 이때 실행되는 함수를 훅이라고 부르기로 정한 것이다.
이러한 훅의 이름은 모두 use로 시작한다.
훅이 수행하는 기능에 따라서 이름을 짓게 되었는데 각 기능을 사용하겠다는 의미로 use를 앞에 붙였다.
개발자가 직접 커스텀 훅을 만들어서 사용할 수도 있는데 커스텀 훅은 개발자 마음대로 이름을 지을 수 있지만, 뒤에서 학습할 훅의 규칙에 따라 이름 앞에 use를 붙여서 훅이라는 것을 나타내주어야 한다.
그렇다면 대표적인 훅들이 무엇이 있는지 알아보자.
useState()
먼저 가장 대표적이고 많이 사용되는 훅인 useState 훅을 알아보겠다.
useState는 이름에서 알 수 있듯이 state를 사용하기 위한 훅이다.
함수 컴포넌트에서는 기본적으로 state라는 것을 제공하지 않기 때문에 클래스 컴포넌트처럼 state를 사용하고 싶으면 useState 훅을 사용해야 한다.
import React, { useState } from "react";
function Counter(props) {
var count = 0;
return (
<div>
<p>총 {count}번 클릭했습니다. </p>
<button onClick={() => count++}>클릭</button>
</div>
);
}
export default Counter;
위 코드는 Counter라는 함수 컴포넌트다.
Counter 컴포넌트는 버튼을 클릭하면 count를 하나씩 증가시키 현재 카운트를 보여주는 단순한 컴포넌트다.
그런데 만약 이처럼 카운트를 함수의 변수로 선언해서 사용하게 되면 버튼 클릭 시 카운트 값을 증가시킬 수는 있지만 재렌더링이 일어나지 않아 새로운 카운트 값이 화면에 표시 되지 않는다.
따라서 이런 경우에는 state를 사용해서 값이 바뀔 때마다 재렌더링 되도록 해야 하는데, 함수 컴포넌트에는 해당 기능이 따로 없기 때문에 useState를 사용하여 state를 선언하고 업데이트해야 한다.
useState 훅은 다음과 같이 사용한다.
const [변수명, set함수명] = useState(초기값);
useState를 노출할 때에는 파라미터로 선언할 state의 초기값이 들어간다.
클래스 컴포넌트의 생성자에서 state를 선언할 때 초기값을 넣어주는 것과 동일한 것이라고 보면 될 것 같다.
이렇게 초기값을 넣어 useState를 노출하면 return값으로 배열이 나온다.
return된 배열에는 두 가지 항목이 들어 있는데 첫 번째 항목은 state로 선언된 변수이고, 두 번째 항목은 해당 state의 set 함수다.
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>
);
}
export default Counter;
해당 코드는 useState를 사용한 Counter 함수 컴포넌트다.
이 코드에서 state의 변수명과 set함수가 각각 count, setCount로 되어 있는것을 볼 수 있다.
버튼이 눌렸을 때 setCount 함수를 호출해서 카운트를 1 증가시키며, 카운트 값이 변경되면 컴포넌트가 재렌더링 되면서 화면에 새로운 카운트 값이 표시된다.
이 과정은 클래스 컴포넌트에서 setState 함수를 호출해서 state가 업데이트되고 이후 컴포넌트가 재렌더링 되는 과정과 동일하다고 보면 된다.
다만 클래스 컴포넌트에서는 setState 함수 하나를 사용해서 모든 state 값을 업데이트 할 수 있었지만 useState를 사용하는 방법에서는 변수 각각에 대해 set 함수가 존재한다는 것을 기억해야 한다.
useEffect()
useState와 같이 가장 많이 사용되는 훅으로 useEffect가 있다.
useEffect는 사이드 이펙트를 수행하기 위한 훅이다.
그렇다면 사이드 이펙트가 무엇인지 먼저 알아봐야겠다.
사이드 이펙트는 사전적으로 부작용이라는 듯을 가지고 있다.
원래 단어의 의미 자체가 부정적인 느낌을 가지고 있는데 프로그래밍에서도 부정적인 의미로 사용되곤 한다.
개발자가 의도치 않은 코드가 실행되면서 버그가 나타나면 사이드 이펙트가 발생했다고 말하는데, 리액트에서의 사이드 이펙트는 부정적인 의미는 아니다.
리액트에서 말하는 사이드 이펙트는 그냥 효과 혹은 영향을 뜻하는 이펙트의 의미에 가깝다.
예를 들면 서버에서 데이터를 받아오거나 수동으로 DOM을 변경하는 등의 작업을 의미하는데, 이런 작업을 이펙트라고 부르는 이유는 이 작업들이 다른 컴포넌트에 영향을 미칠 수 있으며 렌더링 중에는 작업이 완료 될 수 없으며, 렌더링이 끝난 이후에 실행되어야 하는 작업들이기 때문이다.
그래서 이러한 작업들이 사이드로 실행된다는 의미에서 사이드 이펙트라고 불리며 useEffect는 리액트의 함수 컴포넌트에서 사이드 이펙트를 실행할 수 있게 해주는 훅이다.
useEffect는 클래스 컴포넌트에서 제공하는 생명 주기 함수인 componentDidMount, componentDidUpdate, componentWillUnmount와 동일한 기능을 하나로 통합해서 제공한다.
그래서 useEffect 훅만으로 이러한 생명 주기 함수와 동일한 기능을 수행할 수 있다.
useEffect는 다음과 같이 사용한다.
useEffect(이펙트 함수, 의존성 배열);
첫번째 파라미터로는 이펙트 함수가 들어가고 두번째 파라미터로는 의존성 배열이 들어간다.
의존성 배열은 말 그대로 이 이펙트가 의존하고 있는 배열인데 배열 안에 있는 변수 중에 하나라도 값이 변경되었을 때 이펙트함수가 실행된다.
기본적으로 이펙트 함수는 처음 컴포넌트가 렌더링 된 이후와 업데이트로 인한 재렌더링 이후에 실행된다.
만약 이펙트 함수가 mount와 unmount시에 단 한 번씩만 실행되게 하고 싶으면 다음과 같이 의존성 배열에 빈 배열을 넣으면 된다.
useEffect(이펙트 함수, []);
이렇게 하면 해당 이펙트가 props나 state에 있는 어떤 값에도 의존하지 않는 것이 되므로 여러 번 실행되지 않는다.
그리고 의존성 배열은 생략할 수도 있는데 생략하게 되면 컴포넌트가 업데이트 될 때마다 호출된다.
다음은 useEffect를 사용한 예제 코드다.
import React, { useEffect, useState } from "react";
function Counter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `you clicked ${count} times`;
});
return (
<div>
<p>총 {count}번 클릭했습니다. </p>
<button onClick={() => setCount(count + 1)}> 클릭</button>
</div>
);
}
export default Counter;
이 코드는 앞에서 useState를 학습 할 때 살펴본 코드와 거의 동일하며 추가로 useEffect 훅을 사용하며, 클래스 컴포넌트에서 제공하는 componentDidMount, componentDidUpdate와 같은 생명주기 함수의 기능을 동일하게 수행하도록 만들었다.
useEffect 안에 있는 이펙트 함수에서는 브라우저에서 제공하는 API를 사용해서 document의 title을 업데이트한다.
document의 title은 우리가 브라우저에서 페이지를 열었을 때 창에 표시되는 문자열인데, 크롬 브라우저의 경우 탭에 나오는 제목이라고 보면 된다.
이 코드처럼 의존성 배열 없이 useEffect를 사용하면 리액트는 DOM이 변경된 이후에 해당 이펙트 함수를 실행하라는 의미로 받아들인다.
그래서 기본적으로 컴포넌트가 처음 렌더링 될 때를 포함해서 매번 렌더링 될 때마다 이펙트가 실행된다고 보면 된다.
이 코드의 경우 이펙트 함수가 처음 컴포넌트가 마운트 되었을 때 실행되고 이후 컴포넌트가 업데이트 될 때마다 실행된다.
결과적으로 componentDidMount, componentDidUpdate와 동일한 역할을 하게 되는 것이며, 또한 이펙트는 함수 컴포넌트 안에서 선언되기 때문에 해당 컴포넌트의 props와 state에 접근할 수도 있다.
이 코드에서는 count라는 state에 접근하여 해당 값이 포함된 문자열을 생성해서 사용하는 것을 볼 수 있다.
그렇다면 componentWillUnmount와 동일한 기능은 useEffect로 어떻게 구현할 수 있을까?
import React, { useEffect, useState } from "react";
function UserStatus(props) {
const [isOnline, setIsOnline] = useState(null);
function handStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ServerAPI.subscribeUserStatus(props.user.id, handStatusChange);
return () => {
ServerAPI.unsubscribeUserStatus(props.user.id, handStatusChange);
};
});
if (isOnline === null) {
return "대기중";
}
return isOnline ? "온라인" : "오프라인";
}
export default UserStatus;
해당 예제코드를 확인해보자.
이 코드는 useEffect에서 먼저 서버 API를 사용하여 사용자의 상태를 구독하고 있는데, 이후 함수를 하나 return하는데 해당 함수안에는 구독을 해지하는 api를 호출하도록 되어 있다.
useEffect에서 return하는 함수 컴포넌트가 mount 해제, 즉 unmount될 때 호출된다.
결과적으로 useEffect의 return 함수의 역할은 componentWillUnmount 함수가 하는 역할과 동일하다고 볼 수 있다.
또한 useEffect 훅은 하나의 컴포넌트의 여러 개를 사용할 수 있다.
다음 코드는 두 개의 useEffect 훅을 사용하는 예제 코드다.
import { useEffect, useState } from "react";
function UserStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `총 ${count}번 클릭했습니다.`;
});
const [isOnline, setIsOnline] = useState(null);
const ServerAPI = "";
useEffect(() => {
ServerAPI.subscribeUserStatus(props.user.id, handStatusChange);
return () => {
ServerAPI.unsubscribeUserStatus(props.user.id, handStatusChange);
};
});
function handStatusChange(status) {
setIsOnline(status.isOnline);
}
// ...
}
useState 훅과 useEffect 훅을 각각 두 개씩 사용하는 것을 볼 수 있다.
지금까지 학습한 useEffect 훅의 사용법을 다시 자세하게 확인해보자.
useEffect(
() => {
// 컴포넌트가 마운트 된 이후,
// 의존성 배열에 있는 변수들 중 하나라도 값이 변경되었을 때 실행됨
// 의존성 배열에 빈 배열 ([])을 넣으면 마운트와 언마운트시에 단 한 번씩만 실행됨
// 의존성 배열 생략 시 컴포넌트 업데이트 시마다 실행됨
// ...
return () => {
// 컴포넌트가 마운트 해제되기 전에 실행됨
};
},
[
// 의존성 변수1, 의존성 변수2, ...
]
);
기본적으로 이펙트 함수와 의존성 배열이 들어가고 컴포넌트가 마운트된 이후 의존성 배열에 있는 변수들 중 하나라도 값이 변경되었을 때 실행 된다.
그리고 만약 의존성 배열에 빈 배열을 넣으면 마운트와 언마운트 시에 단 한 번씩만 실행된다.
마지막으로 의존성 배열 생략 시에는 컴포넌트 업데이트 시마다 실행된다.
조금 복잡하게 느껴질 수 있는데 useEffect는 굉장히 자주 사용하기 때문에 꼭 이 구조를 반복해 학습하면서 기억해두자.
'밥줄 > React' 카테고리의 다른 글
[React] React 학습 (8) - Hooks (useMemo, useCallback, useRef) (3) | 2024.11.05 |
---|---|
[React] React 에러 해결 - Component Key (2) | 2024.11.04 |
[React] React 학습 (6) - State & Lifecycle (2) | 2024.11.04 |
[React] React 학습 (5) - Component 예제 (2) | 2024.11.01 |
[React] React 학습 (4) - Props (4) | 2024.10.31 |