본문 바로가기
React

useEffect 이해하기

by 붕어사랑 티스토리 2022. 9. 6.
반응형

https://reactjs.org/docs/hooks-effect.html

 

Using the Effect Hook – React

A JavaScript library for building user interfaces

reactjs.org

 

공식문서 설명이 한글로 거지같아서 영어로 읽고 한번 정리하는 내용임

 

 

 

  • useEffect는 class Component의 componentDidMountcomponentDidUpdatecomponentWillUnmount 에 대응되는 개념이다.
  • 많은 케이스에서 componentDidMountcomponentDidUpdate에 똑같은 작업을 동시에 수행하는 경우가 있었다. 이에 페이스북은 useEffect에 두 기능을 한번에 묶어버렸다. 물론 따로 if문으로 두 작업을 분기하는 방법이 있음
  • 특정 변수에만 effect를 사용하고 싶으면 deps를 이용한다
  • deps에 빈 배열을 넣으면 DidMount만 수행 할 수 있다
  • Unmount의 경우 useEffect에서 함수를 리턴하는 형식으로 구현된다.
  • cleanup의 개념은 unmount와 조금 다르다
  • cleanup을 사용하니 Mount->Update->Unmount 라이프 사이클 대비 코드도 깔끔해지고 버그도 줄어든다

 

 

 

 

 

Effects Without Cleanup

보통 리액트가 DOM을 업데이트 한 후 추가적인 작업이 필요한 경우가 있다. 가령 네트워크 리퀘스트라던지, DOM의 직접적인 수정, 또는 로그를 찍는게 대표적인 effects의 예시일 것이다. 그리고 이 예시들은 cleanup을 요구하지 않는다.

 

다시 말하면 위 effects 케이스들은 우리가 수행한뒤 그냥 잊어버리고 살아도 아무 문제 없는 예시들이다.

 

 

 

 

컴포넌트 클래스에서 effects예시

 

컴포넌트 클래스에서 cleanup이 필요없는 effects는 componentDidMountcomponentDidUpdate를 사용한다

 

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

위 코드에서 문제점은 동일한 작업을 두 함수에서 사용하고 있다는 것이다.

앞선 요약에서 언급한 것 처럼 많은 케이스에서 mount와 update에 동일한 작업을 수행하는 경우가 많다.

똑같은 작업을 두 함수에 나눠서 실행하면 코드의 낭비 뿐만 아니라 관리에도 어려움이 있을 것이다.

 

실제로 페이스북에서도 말 하길, mount와 update에 동일한 코드 작업을 하는 경우가 많았다고 한다.

 

 

 

Effect Hook을 사용한 예제

다음은 함수컴포넌트에서 useEffect를 사용한 예제이다

 

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

 

위 예제에서는 useEffect 하나의 함수로 DidMount와 DidUpdate가 묶인것을 볼 수 있다.

즉 위 예제에서는 컴포넌트가 업데이트 될 때 마다 useEffect안에 있는 함수가 호출된다.

 

 

여기서 아래와 같은 궁금증이 생길 수 있다

 

 

 

나는 컴포넌트가 Mount만 될 때만 작업을 수행하고 싶어요!

 

물론 방벙이 있다! 사실 useEffect는 아래와 같은 형태의 함수이다

 

useEffect(function, [deps : effect의 대상이될 변수들 모음]);

 

여기서 function은 effect시 실행될 함수이도 중요한건 deps이다!

 

 

deps

이놈들은 한마디로 effect의 대상이 될 변수들의 모음이다. 앞선 예제를 조금 변형하여 문제 상황을 만들어보자

 

import React, { useState, useEffect } from 'react';

function Example() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count1} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount1(count1 + 1)}>
        Click me1
      </button>
      <button onClick={() => setCount2(count2 + 1)}>
        Click me2
      </button>
    </div>
  );
}

 

앞선예제에서 count를 count1과 count2 두개로 늘렸다. 위 상황에서 나는 count1이 변할때만 로그를 띄우고 싶다. count2를 증가시켜도 useEffect가 호출되게 된다.

 

나는 count1이 변할때만 로그를 호출하고 싶은데 말이다. 이때 해결법은 아래처럼 deps에 count1를 지정해주면 된다!

 

useEffect(() => {
    document.title = `You clicked ${count1} times`;
  }, [count1]);

 

그럼 count1이 변할 때만 로그가 출력된다.

import React, { useState, useEffect } from 'react';

function Example() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count1} times`;
  }, [count1]);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount1(count1 + 1)}>
        Click me1
      </button>
      <button onClick={() => setCount2(count2 + 1)}>
        Click me2
      </button>
    </div>
  );
}

 

 

 

 

자 그럼 앞선 질문, componentDidMount일 때 만 작업을 수행하려면 어떻게 하면 될 까?

 

정답은 deps에 빈배열을 넣어주면 된다!

useEffect(()=>{
  console.log("mount!");
  }, []);

 

 

 

 

 

Effects with Cleanup

앞서 우리는 DidMount와 DidUpdate 하는법을 배웠다. 그럼 Unmout는 어떻게 하는가?

정답은 useEffect안에 함수를 리턴해주면 리턴해주는 함수가 Unmount를 수행한다!

 

허나 한가지 기억해야 할 것이 있다. Cleanup은 unmount와 조금 다른개념이다

 

클래스 컴포넌트에서의 예제

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

 

함수 컴포넌트에서의 예제, useEffect 사용

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

 

 

 

Unmount와 Cleanup의 다른점

Unmount는 컴포넌트가 Unmount될 때 불린다. cleanup도 마찬가지로 Unmount시점에서 호출된다.

하지만 cleanup은 추가로 컴포넌트가 매번 리렌더링 될 때 마다, 다음 effect가 발생하기전에 호출된다.

 

어쨋든 중요한건 리렌더링 될 때 마다 매번 호출된다는 것

 

 

왜 useEffect를 이렇게 만들었을 까?

 

아래 문제상황을 확인하자.

 

앞선 예제에서 Friend를 subscribe하고 unsubscribe 하는 걸 보았을 것이다. 이는 현재 접속하고 있는 친구가 온라인상에 있는지 없는지를 확인하는 api이다

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

 

헌데 중간에 friend의 prop이 바뀌었다면? 친구의 정보는 바뀌었는데 나는 이전에 데이터로 잘못된 친구의 정보를 참조하고 있게된다. 그리고 이는 메모리릭과 언마운트 시점에서 잘못된 정보로 unsubscribe하여 crash를 유발할 수 있다.

 

이를 해결할 방법은 DidUpdate를 활용하여 친구의 정보를 업데이트 하는 것 이다

 

 componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate(prevProps) {
    // Unsubscribe from the previous friend.id
    ChatAPI.unsubscribeFromFriendStatus(
      prevProps.friend.id,
      this.handleStatusChange
    );
    // Subscribe to the next friend.id
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

 

해결됐네? 그럼 도대체 뭐가 문제인가?

 

페이스북에서 말하길, 개발자들이 자주하는 실수로 Mount와 Unmout로직은 만들었는데 Update에서 적절한 예외처리 하는걸 까먹는 경우가 많아 많은 버그들을 만들어낸다고 한다

 

 

 

 

자 다음은 Effect Hook을 이용하여 위 예제를 구현해보자

function FriendStatus(props) {
  // ...
  useEffect(() => {
    // ...
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

깔끔하기 그지 없다.

 

 

 

즉 요약하면 다음과 같다

 

Mount -> Update -> Unmount의 라이프 사이클의 개념을

effect -> cleanup -> effect -> cleanup 이런식으로 바꾸었더니

 

 

코드도 깔끔해지고 버그도 잘 안생기게 되었다!

 

라고 말하는 것이다

 

 

 

 

 

반응형

댓글