Side Effects와 useEffect

Side Effects

리액트의 주요 업무는 UI를 렌더하고 사용자 입력에 반응하는 것이다. 예를 들어 JSX를 평가하고 렌더하거나, state와 props를 관리하거나, 사용자 이벤트나 입력에 반응하는 일 등이 있다.

사이드 이펙트는 애플리케이션에서 일어나는 다른 일을 뜻한다. 예를 들어 서버에 HTTP 요청을 보낸다거나, 브라우저 저장소에 데이터를 저장한다거나, 타이머를 설정하고 관리하는 일 등이 있다. (이메일이나 비밀번호 입력에 대한 응답으로 폼의 유효성을 검사하고 업데이트하는 것도 사이드 이펙트로 볼 수 있다.)

이런 사이드 이펙트는 직접적으로 컴포넌트에 들어가서는 안된다. 버그나 무한루프가 발생할 가능성이 높기 때문이다. 예를 들어 HTTP 요청에 대한 응답으로 state를 변경한다면 무한 루프에 빠질 수 있다. 왜냐하면 컴포넌트가 리렌더링될 때마다 요청을 보내고 응답에 따라 state가 변경되고 또 state 변경으로 인해 컴포넌트가 리렌더링되기 때문이다.

useEffect

useEffect는 사이드 이펙트를 처리하기 위한 리액트 훅이다. useEffect는 두 개의 인수를 받는데, 첫 번째는 컴포넌트가 평가된 이후 의존성 배열이 변경되면 실행되는 함수이고 두 번째는 의존성 배열이다. 두 번째 인수인 의존성 배열에 있는 값이 변경될 때마다 첫 번째 인수에 지정한 함수가 실행된다. 컴포넌트가 리렌더링되는 경우 실행되지 않으며 오로지 의존성이 변경된 경우에만 실행된다. 따라서 사이드 이펙트 코드를 첫 번째 함수 내부에 작성하면 의존성이 변경된 경우에만 실행되기 때문에 버그나 무한루프가 발생할 가능성을 낮출 수 있다.

두 번째 인수인 의존성 배열에 있는 값이 변경될 때마다(+ 첫 렌더링) 지정한 함수가 실행된다고 했는데 만약 의존성 배열이 없거나 비어있는 경우는 어떻게 될까? 만약 두 번째 인수를 생략한다면 리렌더링마다 useEffect가 실행된다. 이는 useEffect를 사용하는 의미가 없으므로 지양해야 한다. 다음으로 의존성 배열 안이 빈 경우 변경될 값이 없으므로 첫 렌더링에서만 useEffect가 실행된다.

Cleanup 함수

useEffect는 함수를 반환할 수 있는데 이걸 Cleanup 함수라고 한다. Cleanup 함수는 컴포넌트가 언마운트될 때나 다음 useEffect가 실행되기 전에 실행된다. useEffect가 처음 실행될 때는 실행되지 않기 때문에 주의해야 한다.

간단하게 이메일과 비밀번호를 입력받는 폼을 생각해보자. 입력받은 이메일에 대한 유효성 검사를 위해 이메일을 한 글자씩 입력할 때마다 유효성 검사를 하는 것은 비효율적이다. 이것보다는 입력하는 도중 일정 시간 동안 입력을 멈추면 그때 유효성 검사를 하는 것이 효율적이다. 이러한 방식을 디바운싱이라고 한다. useEffect에서 Cleanup 함수와 setTimeout을 사용하면 디바운싱을 구현하기 쉽다.

useEffect(() => {
  // setTimeout()이 생성한 타이머를 식별할 때 사용하는 id 반환
  const timeoutID = setTimeout(() => {
    console.log("Checking form validity!");
 
    setFormIsValid(enteredEmail.includes("@") && enteredPassword.trim().length > 6);
  }, 500);
 
  // Cleanup 함수 : 컴포넌트가 언마운트 되거나, 의존성 배열의 값이 변경되어 useEffect가 실행되기 전에 실행(useEffect가 처음 실행되는 경우 제외)
  return () => {
    console.log("Cleanup!");
 
    //  setTimeout()으로 생성한 타임아웃 취소
    clearTimeout(timeoutID);
  };
}, [enteredEmail, enteredPassword]);

위의 코드를 보면 useEffect 내부에서 setTimeout이 호출되고 각 타이머를 식별하는 ID를 반환한다. 이 ID를 Cleanup 함수에서 타이머를 취소하는 clearTimeout에 인수로 전달하고 있다.

코드를 실행하면 첫 렌더링에서 useEffect가 실행되어 타이머가 생성되어 500ms 이후에 콘솔에 "Checking form validity!"가 찍힌다. 이후 이메일란에 한 글자를 입력하면 의존성 배열의 값이 변경됐으므로 useEffect가 실행되는데, 이때 다음 useEffect 전에 실행되는 Cleanup 함수가 실행되어 콘솔에 "Cleanup!"가 찍힌다. 이후 useEffect가 실행되어 타이머가 생성되고 다른 입력이 없다면 500ms 이후 콘솔에 "Checking form validity!"가 찍힌다.

간단히 말하자면 위 코드를 통해 입력이 있으면 타이머를 생성하고 다음 입력이 있을 경우 이전 타이머를 취소한다. 즉, 마지막 입력했을 때의 타이머만 완료될 것이고 이전의 모든 타이머는 지워진다. 의도대로 사용자가 이메일을 입력하다가 이메일 입력이 끝나면 비로소 유효성 검사를 하는 것이다.

정리

  • 리액트에서 사이드 이펙트는 UI를 렌더링하거나 사용자 입력에 반응하는 일 외의 모든 것이다. 예) HTTP 요청, 브라우저 저장소에 데이터 저장
  • 사이드 이펙트는 버그나 무한루프를 발생시킬 수 있기 때문에 컴포넌트에 직접적으로 들어가면 안된다.
  • useEffect는 사이드 이펙트를 처리하기 위한 리액트 훅이다.
  • useEffect를 사용할 때는 두 개의 인수를 전달해야 하는데, 첫 번째는 의존성 배열이 변경됐을 때 실행할 함수이고 두 번째는 의존성 배열이다.
  • Cleanup 함수는 useEffect에서 반환하는 함수로 다음 useEffect가 실행되기 이전에 실행된다.

참고

React 완벽 가이드 with Redux, Next.js, TypeScript

  • React