안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

리액트 (React)

[React] 공식 문서 요약 - Effect Hook 사용하기

피그브라더 2020. 9. 11. 15:28
 

Using the Effect Hook – React

A JavaScript library for building user interfaces

reactjs.org

* React 공식 문서를 꼼꼼히 읽으면서 개인적으로 요약한 내용입니다. 잘못된 내용이 있다면 지적 부탁드립니다.

 

1. 사이드 이펙트와 useEffect() 함수

클래스형 컴포넌트에서는 데이터를 가져오거나, 어떠한 이벤트를 구독하거나, DOM을 직접 조작하는 등의 작업이 가능하다. 이러한 작업들은 자기 자신뿐 아니라 외부에도 영향을 준다는 의미에서 사이드 이펙트(Side Effect) 또는 간단히 이펙트(Effect)라고 부른다. 이때 useEffect() 함수를 사용하면 함수형 컴포넌트에서도 이러한 이펙트를 수행할 수 있도록 해준다. 그리고 일반적으로 클래스형 컴포넌트에서는 이펙트를 componentDidMount() 메소드, componentDidUpdate() 메소드, 혹은 componentWillUnmount() 메소드에서 구현하는데, 같은 맥락의 기능을 여러 개의 메소드에 나눠서 작성하는 것은 유지보수도 힘들고 가독성도 떨어진다. useEffect() 함수를 사용하면 위 세 개의 생명주기 메소드에서 수행할 수 있는 유사한 기능들을 하나의 함수로 묶어서 처리할 수 있게 된다.

 

2. 정리(Clean-up)가 필요 없는 이펙트

  • 클래스형 컴포넌트에서는 사이드 이펙트를 componentDidMount(), componentDidUpdate() 메소드에 둔다. 사이드 이펙트는 React가 DOM을 업데이트한 이후에 수행해야 하기 때문이다. 그런데 이 방식은 동일한(유사한) 코드가 두 개의 메소드에 중복되어 들어간다는 단점이 있다.

  • 함수형 컴포넌트에서는 useEffect() 함수를 호출함으로써 사이드 이펙트를 수행할 수 있다.

  • effect 함수

    • useEffect() 함수의 인자로 전달되는 함수로, 사이드 이펙트를 수행하는 역할을 맡는다.

    • React는 이 함수를 기억했다가 DOM 마운트/업데이트를 수행한 직후에 호출하게 된다. (매 렌더링마다 호출)

    • 컴포넌트 내부에서 정의되기 때문에 컴포넌트의 props 및 state 값에 접근할 수 있다. (JavaScript 클로저 개념 이용)

    • 매 렌더링마다 이전의 effect 함수를 새로운 effect 함수로 교체하고 이를 실행한다. 이것의 장점은 뒤에서 설명하도록 한다.

    • 클래스형 컴포넌트의 componentDidMount(), componentDidUpdate() 메소드와는 달리 useEffect()에서 사용되는 effect 함수는 브라우저가 화면을 업데이트하는 것을 차단하지 않는다. 이로써 앱의 반응성이 향상된다. 실제로 대부분의 effect 함수는 동기적으로 실행될 필요가 없다. 만약 동기적 실행이 필요한 경우가 생긴다면 useLayoutEffect() 함수를 사용하면 된다.

 

3. 정리(Clean-up)가 필요한 이펙트

  • 클래스형 컴포넌트에서는 메모리 누수 방지를 위한 이펙트의 해제를 componentWillUnmount() 메소드에서 구현한다. 그런데 이 방식 또한 같은 맥락의 코드가 componentDidMount(), componentDidUpdate() 메소드와 componentWillUnmount() 메소드에 나눠 들어간다는 단점이 있다.

  • 함수형 컴포넌트에서는 effect 함수가 이펙트의 해제를 위한 함수를 반환하도록 함으로써 이펙트를 해제할 수 있다.

  • React는 effect 함수가 반환하는 함수를 기억했다가, 기존의 이펙트를 해제해야 할 때 그 함수를 실행하게 된다.

  • 이는 이펙트의 수행을 위한 코드와 이펙트의 해제를 위한 코드가 결합도가 높다는 판단 하에 고안된 구조이다. 이로써 이펙트의 수행과 해제를 위한 로직을 가까이 묶어둘 수 있게 된다. 이것이 클래스형 컴포넌트에서의 방식과 대비되는 장점이라 할 수 있다.

  • effect 함수가 반환하는 함수는 컴포넌트가 언마운트 될 때 호출되는 것이 아니라, 렌더링 할 때마다 호출이 된다. 이는 다음 차례의 이펙트를 수행하기 전에 이전의 이펙트를 해제하기 위해서이다. 참고로 이 방식은 버그 방지에도 도움이 되며, 성능 저하가 걱정되는 경우 이펙트를 건너뛰도록 하는 것도 가능하다. 이와 관련해서는 아래에서 다루도록 하겠다.

 

4. 이펙트의 수행 및 해제가 매 렌더링마다 이뤄지는 이유

  • 클래스형 컴포넌트에 익숙하다면, useEffect() 함수를 사용할 때 매 렌더링마다 기존 이펙트가 해제되고 새로운 이펙트가 수행되는 것에 의아해 할 수 있다. 그러나 이러한 디자인은 버그가 적은 컴포넌트를 만드는 데 큰 도움이 된다.

  • props로 전달받은 값에 해당하는 무언가를 구독하는 컴포넌트를 상상해보자. componentDidMount() 메소드로 구독을 할 것이고, componentWillUnmount() 메소드로 구독을 해제할 것이다. 그런데 중간에 props의 값이 바뀌면 어떻게 될까? 이러한 경우를 대비하려면 componentDidUpdate() 메소드를 또 구현해서 기존의 구독을 해제하고 새로운 구독을 하도록 해야 할 것이다. 그런데 많은 경우 이러한 업데이트 로직의 구현을 누락하는 실수를 범하여 버그를 만들어내는 경우가 많다.

  • useEffect() 함수를 이용하는 경우 이와 같은 버그의 가능성을 고려할 필요도 없다. 애초에 업데이트를 포함한 모든 렌더링 시에 기존의 이펙트가 해제되고 새로운 이펙트가 설정되기 때문이다. 따라서 버그 방지에 큰 도움이 된다.

 

5. useEffect() 함수를 사용하는 몇 가지 팁

5-1. 관심사를 구분하려면 여러 번 useEffect() 함수를 호출하는 것이 좋다.

  • 클래스형 컴포넌트의 문제 중 하나가 서로 관련이 있는 로직들은 여러 개의 생명주기 메소드에 들어가 있는데 서로 관련이 없는 로직들은 동일한 생명주기 메소드에 들어가 있다는 것이다. Hook이 등장한 것도 이러한 문제를 해결하기 위함이다.

  • 이때 useEffect() 함수를 여러 번 호출하는 것을 통해 서로 관련이 있는 로직들을 뭉쳐놓을 수 있다. (로직이 N개면 N번 호출)

  • 즉 생명주기 메소드가 아니라 무슨 동작을 수행하는지를 기준으로 코드를 나눌 수 있게 된다.

  • 단, 이와 같이 Hook을 여러 번 호출하는 경우 그 호출 순서는 반드시 매 렌더링마다 동일해야 한다.

 

5-2. 성능 최적화를 위해 이펙트를 건너뛰도록 할 수 있다.

  • 매 렌더링마다 이전 이펙트를 해제하고 새로운 이펙트를 적용하는 것이 때때로 성능 저하를 유발할 수도 있다.

  • 클래스형 컴포넌트의 경우 componentDidUpdate(prevProps, prevState) 메소드에서 prevProps, prevState를 현재의 props, state와 비교함으로써 이러한 문제를 해결할 수 있다.

  • useEffect() 함수에는 이와 같은 비교를 수행할 수 있도록 하는 API를 제공한다.

  • useEffect() 함수의 두 번째 인자로 비교할 값들의 배열(이하 의존성 배열)을 넘겨주면 된다.

  • React는 렌더링을 할 때마다 현재의 의존성 배열을 기억해둔다. 그리고 다음 렌더링 시에 기억해둔 의존성 배열과 새로운 의존성 배열의 요소들을 하나씩 비교한다. 이때 하나의 값이라도 다르다면 effect 함수를 실행하게 된다.

  • effect 함수에 의해 사용되는, 시간에 따라 변하는 컴포넌트 스코프의 값(props, state 등)들은 전부 의존성 배열에 포함시켜야 한다. 그렇지 않으면 렌더링 시 새로운 이펙트를 적용해야 할 때도 과거의 이펙트를 그대로 사용하게 될 수 있다. 이러한 맥락에서, 이펙트가 특정 함수를 호출하는 경우 해당 함수는 effect 함수 내에 지역적으로 정의하는 것이 일반적이다. 그래야 이펙트가 어떠한 값을 사용하는지 한눈에 보기 쉬워지기 때문이다. 이와 관련한 자세한 내용은 여기를 참조하자.

  • 이펙트의 수행 및 해제를 딱 한 번씩만 하고 싶다면(마운트와 언마운트 시) 의존성 배열로서 빈 배열을 사용하면 된다. 그러면 React는 해당 이펙트가 props나 state로부터의 그 어떠한 값에도 의존하지 않아서 재실행될 필요가 없음을 알게 된다. 이처럼 빈 배열을 넘기면 effect 함수 내부에서 사용하는 props와 state는 초깃값을 유지하게 된다. 마치 클래스형 컴포넌트의 componentDidMount() 메소드와 componentWillUnmount() 메소드처럼 말이다.

  • 참고로 의존성 배열이 너무 자주 바뀌는 경우에 대해서는 이곳을 읽어보기 바란다.

  • exhaustive-deps 규칙을 eslint-plugin-react-hooks 패키지에 포함하는 걸 권장한다. 이는 의존성 배열이 올바르게 지정되지 않았을 때 경고 메시지를 띄워주고 올바르게 수정할 수 있도록 알려준다.