개발 R.I.P.

6.28 Dev.Feedback (React #6 useEffect)

편행 2021. 6. 28. 16:22
728x90

Side Effect(부수효과)

함수 내에서 어떤 구현이 함수 외부에 영향을 끼치는 경우 해당 함수는 Side Effect가 있다고 한다.

 

예시를 통해 설명해보면,

let foo = 'hello';

function bar() {
  foo = 'world';
}

bar(); // bar는 Side Effect를 발생시킨다!

bar() 함수는 왜 side effect를 발생시킨다고 할까?

console을 찍어보면 알 수 있다.

콘솔창에서 foo를 찍어보면 알 수 있듯, 전역변수인 foo에 영향을 미친 것을 볼 수 있다.

이렇게 되면, 전체적인 기능 구현에 있어서 엄청난 문제를 일으킬 수도 있다. (우리가 var를 안쓰는 이유와 비슷하다고 느껴진다.)

Pure Function (순수 함수)

순수 함수란, 오직 함수의 입력만이 함수의 결과에 영향을 주는 함수를 의미한다. 함수의 입력이 아닌 다른 값이 함수의 결과에 영향을 미치는 경우, 순수 함수라고 부를 수 없다. 또한 순수 함수는, 입력으로 전달된 값을 수정하지 않는다.

 

Pure함수의 조건

  • 동일한 인자가 들어갈 경우 항상 같은 값이 나와야 한다.(예측이 가능해야 한다.)
  • 부수적인 효과가 일어나면 안 된다.
  • return 값으로만 소통한다.
function multiple(a,b) {
  return a * b
}

multiple(10, 5); // 50
multiple(10, 5); // 50

순수 함수에는 네트워크 요청과 같은 Side Effect가 없다. 순수 함수의 특징 중 하나는, 동일한 인자가 주어질 경우, 항상 똑같은 값이 리턴됨을 보장하는 것이다. 그래서 예측 가능한 함수이기로도 말한다.

 

위와 같은 이유때문에 Math.random()함수는 순수함수로 보지 않는다. 그 이유는  어떤 결과값이 나올지 예측이 불가능하기 때문이다.

또한 어떤 함수가 fetch API를 이용해 AJAX 요청을 한다면, 그 함수 또한 순수함수가 아니게 된다. 그 이유는 네트워크의 상황 서버 상태에 따라 응답 코드가 시시각각 달라질 수 있기 때문이다.

 

React에서 Side Effect

React의 함수 컴포넌트는 순수 함수로 작동한다.
그저 props로 값을 받고 그에 대한 값의 출력만이 하위 컴포넌트에서 구현될 뿐이다.

하지만 React로 애플리케이션을 만들때에는 데이터 가져오기, 구독(subscription) 설정하기, 수동으로 리액트 컴포넌트의 DOM을 수정하는 것까지  이 모든 것들의 예시에서 볼수 있듯 side effects를 일으킬 수 있는 요소들을 가져와 애플리케이션을 만들어야 한다.

 

React 컴포넌트를 하나의 함수라고 봤을 때 이런 상황은 껄끄럽지 않은 상황이다. 계속된 side effect 발생은 상위 컴포넌트에도 영향을 미칠 수 있기 때문이다.

따라서 React는 Side Effect를 다루기 위한 Hook인 Effect Hook을 제공한다.

Effects Without Cleanup(정리를 이용하지 않는 Effect)

우리가 React를 통해 DOM을 업데이트 하고나서도 코드를 추가적으로 실행해야 되는 경우가 있다. 네트워크 요청이라던지 DOM 변경을 한다던지, 또는 로그인 같은 부분들 같은 것들을 예시로 들 수 있다. 이런 부분들은 따로 정리가 필요없어서 우리가 잊어도 되는 부분인데,

React에서는 useEffect를 통해 이를 지원해준다.

 

함수형 컴포넌트를 이용해 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는 어떻게 작동할까?

useEffect Hook을 이용하여 리액트에게 컴포넌트가 렌더링 이후에 어떤 일을 수행해야하는 지를 전달할 수 있다. 리액트는 우리가 넘긴 함수를 기억했다가(이 함수는 ‘effect’라고 부르자) DOM 업데이트를 수행한 이후에 불러낸다.

위의 코드는 effect를 통해 문서 타이틀을 지정하는 예시이다. 이 외에도 데이터를 가져오거나 다른 명령형(imperative) API를 불러내는 일도 할 수 있다.

 

useEffect를 컴포넌트 안에서 불러내는 이유는 무엇일까? 

useEffect를 컴포넌트 내부에 둠으로써 effect를 통해 count state 변수(또는 그 어떤 prop에도)에 접근할 수 있게 된다. 함수 범위 안에 존재하기 때문에 특별한 API 없이도 값을 얻을 수 있는 것이다. Hook은 자바스크립트의 클로저를 이용하여 리액트에 한정된 API를 고안하는 것보다 자바스크립트가 이미 가지고 있는 방법을 이용하여 문제를 해결한다.

 

useEffect는 렌더링 이후에 매번 수행되는 걸까? 

그렇다. 기본적으로 첫번째 렌더링과 이후의 모든 업데이트에서 수행된다.(effect를 원하는 바에 맞게 수정하는 방법도 있다.) 마운팅과 업데이트라는 방식으로 생각하는 대신 effect를 렌더링 이후에 발생하는 것으로 생각하는 것이 더 쉬울 것이다. effect가 수행되는 시점에 이미 그 컴포넌튼는 React를 이용하여 DOM에 업데이트 되어있는 상태이기 때문이다.

Effects with Cleanup(정리(clean-up)를 이용하는 Effects)

위에서 정리(clean-up)가 필요하지 않은 side effect를 보았지만, 정리(clean-up)가 필요한 effect도 있다. 외부 데이터에 구독(subscription)을 설정해야 하는 경우가 그 예중 하나이다. 이런 경우에 메모리 누수가 발생하지 않도록 정리(clean-up)하는 것은 매우 중요하다.

 

질문 ?? 왜 메모리 누수가 발생하는 걸까? 클린업이 필요한 effects에 대한 구독 설정을 제외한 다른 예시들도 찾아보자.

 

구독(subscription)의 추가와 제거를 위한 코드는 결합도가 높기 때문에 useEffect는 이를 함께 다루도록 고안되었다. effect가 함수를 반환하면 리액트는 그 함수를 정리가 필요한 때에 실행시키게 된다.

 

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);
    // effect 이후에 어떻게 정리(clean-up)할 것인지 표시.
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

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

effect에서 함수를 반환하는 이유는 무엇일까? 

이는 effect를 위한 추가적인 정리(clean-up) 메커니즘이다. 모든 effect는 정리를 위한 함수를 반환할 수 있다. 이 점이 구독(subscription)의 추가와 제거를 위한 로직을 가까이 묶어둘 수 있게 한다. 구독(subscription)의 추가와 제거가 모두 하나의 effect를 구성하는 것이다.

리액트가 effect를 정리(clean-up)하는 시점은 정확히 언제일까요? 

리액트는 컴포넌트가 마운트 해제되는 때에 정리(clean-up)를 실행한다. 하지만 위의 예시에서 보았듯이 effect는 한번이 아니라 렌더링이 실행되는 때마다 실행된다. 리액트가 다음 차례의 effect를 실행하기 전에 이전의 렌더링에서 파생된 effect 또한 정리하는 이유가 바로 이 때문이다. 이것이 버그를 방지하는데에 어떻게 도움이 되는지 그리고 성능 저하 문제가 발생할 경우 effect를 건너뛰는 방법에 대해서도 다뤄보고자 한다.

 

useEffect

useEffect는 컴포넌트 내에서 Side effect를 실행할 수 있게 하는 Hook이다.
useState와 함께 리액트의 가장 기본적인 Hook이기도 하다.
문법적 구성은 이렇다.

useEffect(함수, [종속성1, 종속성2, ...])

useEffect의 첫번째 인자는 실행 함수이다. 이 함수 내에서 Side Effect를 실행하면 된다.

  • useEffect의 함수는 세가지 조건에서 실행된다.
  1. 컴포넌트 생성 후 처음 화면에 렌더링될 때(표시)
  2. 컴포넌트에 새로운 props가 전달되며 렌더링될 때
  3. 컴포넌트에 상태(state)가 바뀌며 렌더링될 때

useEffect에 두번째 인자는 종속성 배열이다.
어떤 값의 변경이 일어날 때를 의미하며, 해방 배열엔 어떤 값의 목록이 들어간다.

예를들어서 count라는 state가 있다고 하면 useEffect의 두번째 인자에 [count]를 작성할시,
count가 변경될때마다 첫번째 인자 함수를 실행시킨다.

이러한 조건부 실행에서 기억해야할것이 두가지있는데,

 

첫 번째는 빈 배열을 넣는 것이다.

useEffect(함수, []) => 위를 설정해두면, 컴포넌트가 처음 실행될 때 한 번만 실행되게 된다.

두 번째는 아무것도 넣지 않는 것인데 useEffect(함수) 이 형태가 기본형이다.

해당 컴포넌트에 존재하는 모든 state의 변경마다 use Effect를 새롭게 실행하여 입력되는 값이 계속 변경되게 된다.

'개발 R.I.P.' 카테고리의 다른 글

6.30 Dev.Feedback (HTTP/ Network)  (0) 2021.06.30
6.29 Dev.Feedback (Promise)  (0) 2021.06.29
6.27 Dev.Feedback (Node.js #2)  (0) 2021.06.27
6.26 Dev.Feedback (Node.js)  (0) 2021.06.26
6.25 Dev.Feedback (REST API)  (0) 2021.06.25