TIL#38 React ) useEffect

21920 단어 ReactTILuseEffectReact

Side Effect

Rendering in React

React에서 함수 컴포넌트의 rendering이란 state, props를 기반으로 UI 요소를 그려내는 행위이다.

렌더링의 결과물은 UI 요소, 그러니까 화면에 JSX 문법으로 무엇이 나타날지를 적어둔 컴포넌트들이라고 할 수 있다. 렌더링 결과물에 영향을 주는 요소는 state와 props이다.

이를 달리 표현하면 (state, props) ⇒ UI 라고도 표현할 수 있다. 즉, input(state & props)에 따라 다른 output(UI)을 표현하는 함수(function)와도 구조적으로 동일하다.

Side Effect

컴퓨터 과학에서 함수가 결과값 이외에 다른 상태를 변경시킬 때 부작용이 있다고 말한다. (wikipedia)

부작용, 부수 효과라고도 부르는 Side Effect는 일상 생활의 맥락에서는 부정적인 의미로 사용되는 경우가 많다. 예를 들면 "감기약을 먹었는데 졸음이 오는 부작용이 있다"고 표현하는 경우가 이러한 용법에 해당한다.

하지만 프로그래밍 측면에서의 Side Effect는 단순히 부정적인 의미로만 사용되지 않는다. 위 인용처럼 함수가 어떤 동작을 할 때, input - output 이외의 다른 값을 조작한다면, 이 함수에는 Side Effect(부수 효과) 가 있다고 표현한다.

let count = 0

function greetWithSideEffect(name) { // Input
	count = count + 1 // Side Effect!

	return `${name}님 안녕하세요!` // Output
}

greetWithSideEffect() 라는 함수는 이름을 받아 인삿말을 리턴하는 함수이다.

하지만 이 함수는 단순히 input과 output만 존재하는 함수가 아니다. 실행하는 중간에 함수 외부 세계에 있는 count라는 변수를 조작한다. 이는 함수의 결과값 이외의 다른 상태를 변경시키는 행위에 해당하므로 Side Effect 가 있다고 할 수 있다.

이러한 Side Effect는 React의 함수 컴포넌트에서도 일어날 수 있다.

앞서 함수 컴포넌트의 Input이 state, props이고, output이 UI라고 설명한 바 있다. 그렇다면 함수 컴포넌트의 Side Effect는 state와 props를 받아서 UI를 그려내는 것 이외의 행위라고 할 수 있다.

그렇다면 함수 컴포넌트에서의 Side Effect는, 렌더링이 아니고 외부 세계에 영향을 주는 어떠한 행위이다.

대표적으로 Data Fetching, DOM에 직접 접근(ex. Event Listener 등록), 구독(ex. setInterval)과 같은 행위들이 있다. 이들은 모두 컴포넌트에서 꼭 필요한 대표적인 Side Effect 들이다.

useEffect

side Effect들을 함수의 body 자리(render)에서 실행시키면 안 된다.

앞서 함수 컴포넌트의 리턴 값은 UI 요소이고, state, props의 변화가 있을 때마다 함수가 실행된다고 했다. 이 말은 매 렌더링 때마다 함수 body에 있는 로직이 실행된다는 뜻이다.

또한 렌더링과 무관한 로직이 렌더링 과정에서 실행되기 때문에 렌더링 자체에 영향을 줘 성능상 악영향을 끼칠 수도 있다.

function greetWithSideEffect({ name }) { // Input
  // Bad!
  document.title = `${name}님 안녕하세요!`; // Side Effect

  return <div>{`${name}님 안녕하세요!`}</div>; // Output
}

그래서 React에서는 이런 Side Effect를 일으키기 적절한 장소로서 useEffect hook을 제공한다.

공식 문서에서도 useEffect를 "React의 순수한 함수적인 세계에서 명령적인 세계로의 탈출구로 생각하세요" 라고 설명하고 있다. 여기서 "순수한 세계"란 렌더링(Input → Output)을 뜻하고, 렌더링 이외에 일으켜야 하는 Side Effect를 일으킬 탈출구로 useEffect를 사용하라는 의미이다.

useEffect는 Side Effect를 렌더링 이후에 발생시킨다. (예외: useLayoutEffect). useEffect가 수행되는 시점에 이미 DOM이 업데이트 되었음을 보장한다는 뜻이고, 바꿔 말하면 Side Effect가 렌더링에 영향을 주지 않도록 설계되었음을 의미한다.

import { useEffect } from 'react';

function greetWithSideEffect({ name }) { // Input
  **useEffect(() => {
    // Good!
    document.title = `${name}님 안녕하세요!`; // Side Effect
  }, [name]);**

  return <div>{`${name}님 안녕하세요!`}</div>; // Output
}

만약 Side Effect 이후 업데이트 된 정보가 있어 새롭게 화면이 그려져야 할 경우(state, props의 업데이트) 새롭게 렌더링을 일으킨다.

함수 컴포넌트는 최신 state와 props를 반영한 화면을 리턴하게 된다. Effect를 일으킬 타이밍은 앞서 설명했던 useEffect의 두 번째 인자인 의존성 배열(Dependancy Array)를 통해 표현하게 된다.

Rendering Cycle with useEffect

useEffect는 Dependency가 있던 없던, 대괄호만 있던, 렌더링 후 무조건 한 번 실행된다! useEffect는 렌더링 또는 변수의 값이나 오브젝트가 달라지게 되면 그것을 업데이트 해주는 함수이다. useEffect는 콜백 함수를 부르게 되며 변경 사항에 따라 하나 또는 여러 개의 함수를 동작시킬 수 있다.

useEffect 사용하는 방법

import { useEffect } from "react"

// 사용법
useEffect( 실행시킬 동작, [ 타이밍 ] )
document.addEventListener("타이밍", 실행시킬 동작) // 추상화 한 예시

// 매 렌더링마다 Side Effect가 실행되어야 하는 경우
1. useEffect(() => {
  // Side Effect
})

// Side Effect가 첫 번째 렌더링 이후 한번 실행 되고,
// 이후 특정 값의 업데이트를 감지했을 때마다 실행되어야 하는 경우
2. useEffect(() => {
  // Side Effect
}, [value])

// Side Effect가 첫 번째 렌더링 이후 한번 실행 되고,
// 이후 어떤 값의 업데이트도 감지하지 않도록 해야 하는 경우
3. useEffect(() => {
  // Side Effect
}, [])
  1. useEffect의 가장 기본 형태지만 거의 사용하지 않는다.
    → Dependency가 없기 때문에 렌더링 할 때 한 번만 일어남. 그리고 어떤 변화가 생기면 생기는 족족 useEffect가 작동하여 불필요한 실행이 많아지기 때문에!

  2. useEffect를 렌더링 후 한 번, 그리고 배열 안의 변수 값이 변할 때마다 실행한다. Dependency를 지정해주어 지정된 변수의 값이 변했을 때만 실행되게 된다.

  3. useEffect를 렌더링 후 단 한 번만 실행하고 싶을 때 사용한다. 콜백 함수 뒤에 배열을 나타내는 대괄호가 있고 이 곳에 Dependency를 지정한다. 하지만 아무 변수나 값 없이 대괄호만 있다면 렌더링 후 단 한 번만 실행되고 다시는 실행되지 않는다.

아래 다이어그램은 useEffect를 사용한 렌더링 사이클과 그 경우의 수를 표현하고 있다.

함수 컴포넌트의 렌더링은 기본적으로 아래 순서대로 일어난다.

  1. 컴포넌트가 렌더링된다. 최초로 진행되는 렌더링은 브라우저에 처음으로 이 컴포넌트가 보여졌다는 의미로 mount 라고 표현한다.
  2. useEffect 첫 번째 인자로 넘겨준 함수(callback)가 실행된다. (Side Effect).
  3. 다시 렌더링(re-render)이 일어난다. (stateprops가 변경된 경우)
  4. useEffect는 두 번째 인자에 들어있는 의존성 배열을 체크한다.
    1) 만약 두 번째 인자에 아무런 값도 넘기지 않았거나 / 인자로 넘긴 배열에 들어있는 값 중 업데이트된 것이 하나라도 있다면 첫 번째 인자로 넘겨준 함수(callback)가 실행된다. (Side Effect).
    2) 하나도 없거나 빈 배열이라면, 아무런 일도 하지 않는다.
  5. 만약 앞에서 일으킨 Effect에서 state나 props를 변경시켰다면 다시 렌더링이 일어난다.
  6. (중략...)
  7. 컴포넌트가 필요 없어지면 화면에서 사라진다. 컴포넌트가 브라우저의 화면에서 사라졌다는 의미로 unmount라고 표현한다.

Clean up Effect

Render → Effect Callback → Clean Up!

Cleanup Effect 는 간단하게 설명하자면, 이전에 일으킨 Side Effect를 정리할 필요가 있을 때 사용한다.

앞에서 예시로 들었던 Side Effect 중 이벤트 리스너 케이스를 다시 살펴보자. 다음 코드는 페이지에 스크롤 이벤트가 일어날 때마다 console에 현재 스크롤이 위치한 Y 좌표를 출력한다.

Side Effect이므로 useEffect 안에서 사용하고, 이벤트 리스너는 한번만 등록하면 되기 때문에 의존성 배열에는 빈 배열을 넣어주었다.

useEffect(() => {
	function handleScroll() {
		console.log(window.scrollY)
	}
	document.addEventListener("scroll", handleScroll)
}, [])

하지만 우리가 이 페이지를 벗어났을 때 이 이벤트 리스너는 더 이상 필요없어질 수 있다. 이 경우 우리가 일으켰던 Effect를 정리해줘야 한다. 이때마다 Cleanup Effect를 일으킬 수 있도록 useEffect 안에 해당 로직을 정리하는 동작을 정의해두면 된다.

useEffect(() => {
	function handleScroll() {
		console.log(window.scrollY)
	}

	document.addEventListener("scroll", handleScroll)
	return () => {
		document.removeEventLisnter("scroll", handleScroll)
	}
}, [])

주의할 점은 단순히 컴포넌트가 생성되고, 사라지는 시점에만 Cleanup Effect가 실행되는 건 아니라는 것이다. 다음 Effect가 일어나기 전에, 이전 Effect의 영향을 정리해줘야 한다는 컨셉을 꼭 기억하자.

다음 예시 코드는 state가 업데이트 될 때마다 Effect를 일으키고, 다음 Effect가 일어나기 전에 Cleanup Effect를 일으킨다.

const App = () => {
  const [count, setCount] = useState(0);

  console.log("render", count);

  useEffect(() => {
    console.log("useEffect Callback", count);
    return () => {
			console.log("cleanUp", count);
		});
  }, [count]);

  return <div onClick={() => setCount(count + 1)}>하잉</div>;
};

export default Foo;
  • 위의 코드에서 console을 찍으면?
    다음과 같은 결과가 출력된다.
render, 0
useEffect Callback, 0

// 클릭

render, 1
cleanUp, 0
useEffect Callback, 1


RenderClean Up, Effect 사이의 관계에 주의하여 살펴보자.

참고) https://github.com/donavon/hook-flow

FAQ

  • 하나의 useEffect 안에서 로직을 처리하기 힘들어요! (공식 문서)
    여러 개의 useEffect를 사용하면 된다.

팁 : 관심사를 구분하려고 한다면 Multiple Effect를 사용한다
Hook이 탄생한 동기가 된 문제 중의 하나가 생명주기 class 메소드가 관련이 없는 로직들은 모아놓고, 관련이 있는 로직들은 여러 개의 메소드에 나누어 놓는 경우가 자주 있다는 것이다.
(...) Hook을 이용하여 이 문제를 어떻게 해결할 수 있을까? State Hook을 여러 번 사용할 수 있는 것처럼 effect 또한 여러 번 사용할 수 있다. Effect를 이용하여 서로 관련이 없는 로직들을 갈라놓을 수 있다.

Using the Effect Hook - React

좋은 웹페이지 즐겨찾기