이제부터는 리액트의 또 다른 중요한 Hook인 useEffect에 대해 알아보려고 한다.
특히 클래스형 컴포넌트의 생명주기 메서드들을 함수형 컴포넌트에서는 어떻게 구현하는지 살펴보자 👀
useEffect는 클래스형 컴포넌트의 componentDidMount, componentDidUpdate, componentWillUnmount를 하나로 통합한 Hook이다.
간단히 말해서, 컴포넌트가 렌더링된 이후에 어떤 일을 수행해야 하는지를 정의하는 Hook이라고 볼 수 있다.
클래스형 컴포넌트에서는 각 메서드들을 오버라이딩해서 사용해야했다면, 함수형 컴포넌트에서는 useEffect 훅만으로도 컴포넌트 생명주기에 간섭하여 개발자가 원하는 로직을 실행시킬 수 있다!
useEffect의 기본적인 구조는 다음과 같다.
useEffect 구조
useEffect(콜백 함수, 의존성 배열); // 기본 형태 useEffect(() => { // 수행하고 싶은 작업 정의 }, [의존성1, 의존성2, ...]);
즉, 콜백 함수와 의존성 배열을 이용해서 useEffect훅을 사용할 수 있다.
그렇다면 콜백함수와 의존성 배열은 어떤 의미일까?
1. 콜백 함수
- 개발자가 특정 생명주기에 수행하고 싶은 로직을 함수 형태로 정의한다.
- 이때, useEffect 내부에서 훅을 호출할 순 없다.
- 왜냐하면 훅의 호출 순서가 꼬이기 때문이다!
- 클린업 작업도 콜백 함수 내부에서 정의된다.
2. 의존성 배열
- useEffect 훅은 의존성 배열에 전달된 대상을 listening한다.
- 의존성 배열에 저장된 대상이 update되면 미리 정의한 콜백 함수가 호출된다.
useEffect는 의존성 배열을 어떻게 설정하느냐에 따라 다양한 방식으로 동작한다.
useEffect 훅은 의존성 배열을 생략하여 정의할 수 있다.
Sample Code
function MyComponent() { const [count, setCount] = useState(0); useEffect(() => { console.log("렌더링이 완료될 때마다 실행!"); }); return <div>{count}</div>; }
위 코드처럼 의존성 배열을 생략하면 컴포넌트가 마운트되거나 업데이트될 때마다 DOM 변경이 완료된 후 이펙트 함수가 실행된다.
클래스형 컴포넌트로 나타내면 아마 다음과 같을 것 같다.
Sample Code
class MyComponent extends Component { componentDidMount() { console.log("최초 마운트 시 실행!"); } componentDidUpdate() { console.log("업데이트 시 실행!"); } }
의존성 배열에 아무값도 넣어주지 않으면 어떻게 될까?
Sample Code
useEffect(() => { console.log("마운트 될 때만 실행!"); }, []);
이렇게 정의하면 컴포넌트가 처음 마운트될 때만 이펙트 함수가 실행된다.
클래스형 컴포넌트로 정의하면 아마 다음과 같을 것 같다.
Sample Code
class MyComponent extends Component { componentDidMount() { console.log("최초 마운트 시 실행!"); } }
의존성 배열에 값을 추가한 예시는 다음과 같다.
Sample Code
function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { console.log(`userId가 ${userId}로 변경되어 API 호출!`); fetchUser(userId); }, [userId]); return <div>{user?.name}</div>; }
위 코드는 의존성 배열에 있는 값이 변경될 때마다 이펙트 함수가 실행된다.
이전 예제와는 다르게 조금 복잡하게 작성해봤다.
클래스형 컴포넌트로 바꿔보면 아마 다음과 같을 것 같다.
Sample Code
class UserProfile extends Component { constructor(props) { super(props); this.state = { user: null }; } // 최초 마운트 시 실행 componentDidMount() { console.log(`userId가 ${this.props.userId}로 설정되어 API 호출!`); this.fetchUser(this.props.userId); } // props나 state가 변경될 때 실행 componentDidUpdate(prevProps) { // [중요] userId가 변경되었을 때만 API 호출 if (prevProps.userId !== this.props.userId) { console.log(`userId가 ${this.props.userId}로 변경되어 API 호출!`); this.fetchUser(this.props.userId); } } fetchUser(userId) { // API 호출 로직 } render() { return <div>{this.state.user?.name}</div>; } }
이처럼 컴포넌트가 마운트 될 때 1번, 그리고 userId가 변경되었을 때만 componentDidUpdate가 호출된다고 생각하면 된다!
useEffect에서 콜백함수의 return문에 반환하는 함수를 클린업 함수라고 한다.
이는 컴포넌트가 언마운트되거나 의존성 배열 값 변경 & 리렌더링 직전에 실행된다.
클린업 함수 사용 방법은 다음과 같이 콜백함수 내부에서 return문에 함수를 넘겨주면 된다.
Sample Code
import { useEffect, useState } from "react"; export default function App() { const [count, setCount] = useState(0); useEffect(() => { console.log("useEffect 실행"); return () => { console.log("useEffect Clean-Up 실행"); }; }, [count]); return ( <div> <p>Current Count : {count}</p> <button onClick={() => setCount(count + 1)}>+1</button> </div> ); }
Console View
Console에 찍히는 내용과 같이 의존성 배열의 값 변경 & 리렌더링 발생이라는 조건을 충족할 때마다 클린업 함수가 수행됨을 확인할 수 있다.
여기서 클린업 함수의 실행 시점을 정리하면 다음과 같다.
클린업 함수 실행 시점
- useEffect의 콜백 함수가 실행되기 직전
- 컴포넌트가 unmount되기 바로 직전
즉, 위처럼 의존성 배열에 값을 추가하고 클린업 함수를 정의히면 componentWillUnmount의 역할과 componentDidUpdate 직전의 정리 작업을 모두 수행하게 된다.
(클래스형 컴포넌트의 경우 오직 컴포넌트가 unmount되는 시점에만 componentWillUnmount()가 수행되었다)
오직 컴포넌트의 unmount 시점에만 clean-up 함수가 실행되길 원한다면 의존성 배열을 빈 배열로 선언하면 된다.
빈배열로 선언한다는 것은 최초 마운트 시점에만 콜백함수가 호출됨을 의미한다.
여기서 클린업 함수의 실행 시점을 다시 생각해보면 콜백 함수가 다시 실행되기 직전 혹은 컴포넌트가 unmount되기 직전이다.
의존하는 값이 없으니 컴포넌트가 unmount되기 전까지 useEffect의 콜백 함수는 앞으로 쭉 호출될 일이 없다.
그렇기때문에 오직 컴포넌트가 unmount되는 시점에만 클린업 함수가 호출되는 것이다.
예시 코드를 한번 살펴보자
Sample Code
function ChatRoom() { const [count, setCount] = useState(0); useEffect(() => { console.log("채팅방 입장"); return () => { console.log("채팅방 퇴장"); }; }, []); return ( <div> 채팅방 <button onClick={() => setCount(count + 1)}> ChatRoom 컴포넌트 리렌더링 버튼 </button> </div> ); }
export default function App() { const [isVisible, setIsVisible] = useState(false); return ( <div> <button onClick={() => setIsVisible(!isVisible)}> 채팅방 {isVisible ? "숨기기" : "보이기"} </button> {isVisible && <ChatRoom />} </div> ); }
Console View
위 코드의 경우 컴포넌트가 언마운트 (isVisible === false)가 되는 시점에만 콘솔에 "채팅방 퇴장"이 찍히는 것을 볼 수 있다.
리렌더링 버튼을 누르더라도 useEffect의 콜백함수는 호출되지 않기때문에, 오직 unmount되는 경우가 아니면 클린업 함수가 호출되지 않는 것을 볼 수 있다!
한 가지 주의할 점은 useEffect는 스냅샷으로 동작한다는 것이다.
즉, 클린업 함수는 이펙트가 실행될 당시의 값들을 기억한다.
Sample Code
import { useEffect, useState } from "react"; export default function App() { const [count, setCount] = useState(0); useEffect(() => { console.log("Current Count : ", count); return () => { console.log("Prev Count : ", count); }; }, [count]); return ( <div> <button onClick={() => setCount(count + 1)}>Count + 1</button> </div> ); }
Console View
버튼을 한번 클릭한 결과를 찍어봤다.
이처럼 클린업 함수는 컴포넌트를 unmount하기 전의 상태를 snapshot 형태로 기억하기 때문에 코드 작성할 때 유의하자!
이번에는 useEffect 훅에 대해서 간단하게 살펴봤다.
React Component 인터페이스에 정의된 메서드들을 useEffect와 매칭해서 생각해보면 조금 더 이해하기 편할 것 같다.
만약, useEffect를 많이 사용하지 않았다면 호출이 생각대로 안되는 경우가 있을 수 있다.
만약 그렇다면, 링크를 하나 첨부할테니 이 내용을 읽어보도록 하자!