UseEffect 완벽 가이드 (내맘대로 요약 Fin) (By. Dan Abramov)

Fizz·2022년 9월 17일
0

https://overreacted.io/ko/a-complete-guide-to-useeffect/

함수도 데이터 흐름의 일부인가?

위 패턴은 클래스 컴포넌트에서 사용할 수 없다. 이는 패러다임의 차이를 보여준다.

class Parent extends Component {
  state = {
    query: 'react'
  };
  fetchData = () => {
    const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query;
    // ... 데이터를 불러와서 무언가를 한다 ...
  };
  render() {
    return <Child fetchData={this.fetchData} />;
  }
}

class Child extends Component {
  state = {
    data: null
  };
  componentDidMount() {
    this.props.fetchData();
  }
  render() {
    // ...
  }
}

useEffect 가 componentDidMount와 componentDidUpdate가 섞인 거라는걸 안다. 이 로직은 componentDidUpdate에서는 동작하지 않는다.

class Child extends Component {
  state = {
    data: null
  };
  componentDidMount() {
    this.props.fetchData();
  }
  componentDidUpdate(prevProps) {
    // 🔴 이 조건문은 절대 참이 될 수 없다
    if (this.props.fetchData !== prevProps.fetchData) {
      this.props.fetchData();
    }
  }
  render() {
    // ...
  }
}

fetchData는 클래스 메소드(혹은 속성) 이기 떄문에 state가 달라진다고 메서드는 달라지지 않는다. prop.fetchData 는 prevProps.fetchData와 같다.
만약 조건문을 뺀다면, 렌더링을 할때마다 데이터를 불러오게된다.
Child 컴포넌트로 query자체를 넘기면 CHild가 query를 직접 사용하지 않음에도
query가 바뀔때 다시 데이터를 불러오는 로직은 해결 가능하다.

class Parent extends Component {
  state = {
    query: 'react'
  };
  fetchData = () => {
    const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query;
    // ... 데이터를 불러와서 무언가를 한다 ...
  };
  render() {
    return <Child fetchData={this.fetchData} query={this.state.query} />;
  }
}

class Child extends Component {
  state = {
    data: null
  };
  componentDidMount() {
    this.props.fetchData();
  }
  componentDidUpdate(prevProps) {
    if (this.props.query !== prevProps.query) {
      this.props.fetchData();
    }
  }
  render() {
    // ...
  }
}

클래스 컴포넌트에서 함수 prop자체는 실제로 데이터흐름에서 어떠한 부분도 차지하지 않는다. 메소드는 가변성이 있는this변수에 묶여 일관성을 담보할수 없다. 그러므로 차이를 비교하기위해서는 모든 데이터를 전달해야 한다.
반면에 useCallback을 사용하면 함수는 데이터흐름에 포함된다. props.fetchData같은 변화는 하위컴포넌트에 전달되게 된다.

비슷하게 useMemo 또한 복잡한 객체에 대해 같은 방식의 해결책을 제공한다.

function ColorPicker() {
  // color가 진짜로 바뀌지 않는 한
  // Child의 얕은 props 비교를 깨트리지 않는다
  const [color, setColor] = useState('pink');
  const style = useMemo(() => ({ color }), [color]);
  return <Child style={style} />;
}

useCallback을 남발하기 보다는 훅자체가 콜백을 내려보내는걸 피하는걸 제공하기 떄문에 상황에 맞춰야 한다.
Dan은 위의 예제의 경우 fetchData를 이펙트 안에 두거나, 커스텀훅으로 추상화하거나, import하는것을 선호한다고 한다. 그는 이펙트를 단순하게 만들고 싶어하고, 그안에 콜백을 두는것은 단순하지 않다고 한다. 또한 이러한 방식이 클래스 컴포넌트의 동작을 흉내낼수는 있지만 경쟁상태(race condition)를 해결할수는 없다고 한다.

경쟁 상태에 대해

클래스로 데이터를 불러오는 예제다.

class Article extends Component {
  state = {
    article: null
  };
  componentDidMount() {
    this.fetchData(this.props.id);
  }
  componentDidUpdate(prevProps) {
    if (prevProps.id !== this.props.id) {
      this.fetchData(this.props.id);
    }
  }
  async fetchData(id) {
    const article = await API.fetchArticle(id);
    this.setState({ article });
  }
  // ...
}

하지만 순서를 보장할 수 없기때문에 버그가 난다.
id를 10 > 20 으로 바꾼다면, 20의 요청이 먼저 시작된다. 그래서 먼저 시작된 요청이 더늦게 끝나 잘못된 상태를 덮어씌울 수 있다. 이를 경쟁상태라고 한다. (보통 비동기 호출이 결과를 기다린다 생각) async/await에서 흔히 발생한다. 데이터가 흘러간다는건 prop이나 state가 async에서 바뀔수 있다는 뜻이다.
이펙트가 이를 마법처럼 해결하지는 않는다 (또한 async를 직접적으로 전달하면 경고)

하지만 취소기능을 지원한다면 클린업함수에서 비동기를 취소할 수 있다.
그 예시다

function Article({ id }) {
  const [article, setArticle] = useState(null);

  useEffect(() => {
    let didCancel = false;
    async function fetchData() {
      const article = await API.fetchArticle(id);
      if (!didCancel) {
        setArticle(article);
      }
    }

    fetchData();

    return () => {
      didCancel = true;
    };
  }, [id]);

  // ...
}

https://www.robinwieruch.de/react-hooks-fetch-data/
이 글은 어떻게 에러를 다루고 상태를 불러올지, 어떻게 커스텀 훅으로 추상화 할지 소개한다.

진입장벽을 더 높이기

클래스 컴포넌트의 라이플 사이클 개념으로 생각하면 사이드 이펙트는 랜더링 결과물과 다르게 동작한다. UI와 다르다.

useEffect의 개념으로는 모든것들은 동기화 되고, 사이드 이펙트도 데이터 흐름의 일부가 된다. 고로 더 쉽게 다룰 수 있다.

Dan은 useFetch, useTheme같은 자신만의 훅을 만들어 쓰는 더 높은 수준의 추상화된 훅을 바란다고 한다. 하지만 아직 이수준은 되지 못한다.

아직까지는 useEffect가 주된 페칭사용이다. 하지만 데이터 페칭은 엄밀히 동기화 문제는 아니고 deps 가 [] 인게 문제라고 한다. 우리는 무엇을 동기화 하고 있을까

https://reactjs.org/blog/2018/11/27/react-16-roadmap.html#react-16x-mid-2019-the-one-with-suspense-for-data-fetching
suspense가 도입되면 해결되는 일이다. 하지만 그전까지는 CustomHook이 가장 좋은방법이라 한다.

요약

이건 그대로 가져왔다
TLDR (Too Long; Didn’t Read - 요약)
글 전체를 읽고 싶지 않은 분들을 위해 여기 요약본을 마련해 두었습니다. 읽다가 어떤 부분이 잘 이해가 되지 않으셨다면 연관된 부분이 나올 때까지 스크롤을 아래로 내려보세요.

만약 글 전체를 읽으실 거라면 편하게 스킵하셔도 됩니다. 이 부분으로 이동하는 링크를 글 끝에 달아 두겠습니다.

🤔 질문: useEffect 로 componentDidMount 동작을 흉내내려면 어떻게 하지?

완전히 같진 않지만 useEffect(fn, []) 으로 가능합니다. componentDidMount 와 달리 prop과 state를 잡아둘 것입니다. 그래서 콜백 안에서도 초기 prop과 state를 확인할 수 있습니다. 만약 “최신의” 상태 등을 원한다면, ref에다 담아둘 수는 있습니다. 하지만 보통 더 간단하게 코드를 구조화하는 방법이 있기 때문에 굳이 이 방법을 쓸 필요도 없습니다. 이펙트를 다루는 멘탈 모델은 componentDidMount 나 다른 라이프사이클 메서드와 다르다는 점을 인지하셔야 합니다. 그리고 어떤 라이프사이클 메서드와 비슷한 동작을 하도록 만드는 방법을 찾으려고 하면 오히려 혼란을 더 키울 뿐입니다. 더 생산적으로 접근하기 위해 “이펙트 기준으로 생각해야(thinking in effects)” 하며 이 멘탈 모델은 동기화를 구현하는 것에 가깝지 라이프사이클 이벤트에 응답하는 것과는 다릅니다.

🤔 질문: useEffect 안에서 데이터 페칭은 어떻게 해야할까? 두번째 인자로 오는 배열([]) 은 뭐지?

이 링크의 글이 useEffect 를 사용하여 데이터를 불러오는 방법을 파악하는데 좋은 기본서가 됩니다. 글을 꼭 끝까지 읽어보세요! 지금 읽고 계시는 글처럼 길지 않습니다. [] 는 이펙트에 리액트 데이터 흐름에 관여하는 어떠한 값도 사용하지 않겠다는 뜻입니다. 그래서 한 번 적용되어도 안전하다는 뜻이기도 합니다. 이 빈 배열은 실제로 값이 사용되어야 할 때 버그를 일으키는 주된 원인 중 하나입니다. 잘못된 방식으로 의존성 체크를 생략하는 것 보다 의존성을 필요로 하는 상황을 제거하는 몇 가지 전략을(주로 useReducer, useCallback) 익혀야 할 필요가 있습니다.

🤔 질문: 이펙트를 일으키는 의존성 배열에 함수를 명시해도 되는걸까?

추천하는 방법은 prop이나 state를 반드시 요구하지 않는 함수는 컴포넌트 바깥에 선언해서 호이스팅하고, 이펙트 안에서만 사용되는 함수는 이펙트 함수 내부에 선언하는 겁니다. 그러고 나서 만약에 랜더 범위 안에 있는 함수를 이펙트가 사용하고 있다면 (prop으로 내려오는 함수 포함해서), 구현부를 useCallback 으로 감싸세요. 왜 이런걸 신경써야 할까요? 함수는 prop과 state로부터 값을 “볼 수” 있습니다. 그러므로 리액트의 데이터 플로우와 연관이 있지요. 자세한 답변은 훅 FAQ 부분에 있습니다.

🤔 질문: 왜 가끔씩 데이터 페칭이 무한루프에 빠지는걸까?

이펙트 안에서 데이터 페칭을 할 때 두 번째 인자로 의존성 배열을 전달하지 않았을 때 생길 수 있는 문제입니다. 이게 없으면 이펙트는 매 랜더마다 실행됩니다. 그리고 state를 설정하는 일은 또 다시 이펙트를 실행하죠. 의존성 배열에 항상 바뀌는 값을 지정해 두는 경우에도 무한 루프가 생길 수 있습니다. 하나씩 지워보면서 어느 값이 문제인지 확인할 수도 있지만, 사용하고 있는 의존 값을 지우는 일은(아니면 맹목적으로 [] 을 지정하는 것은) 보통 잘못된 해결법입니다. 그 대신 문제의 근원을 파악하여 해결해야 합니다. 예를 들어 함수가 문제를 일으킬 수 있습니다. 그렇다면 이펙트 함수 안에 집어넣거나, 함수를 꺼내서 호이스팅 하거나, useCallback 으로 감싸서 해결할 수 있습니다. 객체가 재생성되는 것을 막으려면 useMemo 를 비슷한 용도로 사용할 수 있습니다.

🤔 질문: 왜 가끔씩 이펙트 안에서 이전 state나 prop 값을 참조할까?

이펙트는 언제나 자신이 정의된 블록 안에서 랜더링이 일어날 때마다 prop과 state를 “지켜봅니다”. 이렇게 하면 버그를 방지할 수 있지만 어떤 경우에는 짜증날 수 있습니다. 그럴 때는 명시적으로 어떤 값을 가변성 ref에 넣어서 관리할 수 있습니다(링크에 있는 글 말미에 설명되어 있습니다). 혹시 기대한 것과 달리 이전에 랜더링될 때의 prop이나 state가 보인다면, 아마도 의존성 배열에 값을 지정하는 것을 깜빡했을 겁니다. 이 린트 규칙을 사용하여 그 값을 파악할 수 있도록 연습해 보세요. 며칠 안으로 자연스레 몸에 밸 것입니다. 또한 FAQ 문서에서 이 답변 부분을 읽어보세요.

profile
성장하고싶은 개발자

0개의 댓글