[React] 부수효과를 처리하는 UseEffect

29707 단어 ReactReact

useEffect, 단어를 뜯어보면 효과를 사용한다는 것이다. 그런데 여기서 말하는 효과는 무슨 효과를 사용하는 것이지? 🤔 바로 side effect를 처리해주는 효과를 사용한다는 것이다.

그렇다면 Side Effect란 무엇일까?

일상생활에서 부작용은 ‘코로나 부작용...’등의 문맥에 사용되며 부정적인 의미로 사용된다. 그러나 한자 그대로 살펴보면 그저 “부수적인 작용"을 의미하며, 프로그래밍에서의 부작용은 함수가 어떤 동작을 할 때, input - output 이외의 다른 값을 조작한다면, 이 함수에는 Side Effect(부수 효과)가 있다고 표현한다.

리액트에서의 부작용을 생각해본다면, state와 props를 받아 UI를 그리는 주 기능( function rendering = (state, props) => UI ) 외에 일어나는 모든 부수적인 효과를 side effect라고 할 수 있겠다.

이렇게만 들으면 그래서 대체 부작용이 뭔데? 라고 생각할 수 있으니 일반적인 함수를 사용하는 상황에서의 Side Effect의 예시 몇 가지만 쓱 보고 가자.

// 예시 1. console.log 부분이 side Effect라고 할 수 있겠다.

const sumOne = (num) => {
	console.log("input", num)
	return num + 1
}

sumOne(1)
// 예시 2. 아래 함수는 side effect가 없는 함수인가?
// 아니! 있는 함수이다.
// input과 output 외의 다른 값을 조작하기 때문에!
// input과 output 외에 외부의 변수 값을 읽어왔기 때문에 side effect를 일으키는 함수가 맞다.

const addNum = 30;
const sumNum = (num) => {
	return num + addNum
}
// 예시 3. 아래 함수는 side effect가 없는 함수인가?
// 아니! 있는 함수이다.
// input과 output 외의 다른 값을 조작하기 때문에!
// input과 output 외에 외부의 변수 값을 변경했기 때문에!! 조작해버렸기 때문에!!

let variable = 10;

const sumVar = (num) => {
	variable = 20;
	return num+1
};

결론적으로, 전반적인 프로그래밍에서 input으로 받은 값을 조작해서 output으로 return해주는 것 외의!!! 모든 것!!! (외부의 변수를 읽는다든지 변경한다든지, 콘솔에 찍는다든지 하는 것)은 전부 side effect이다.

동일한 논리를 리액트의 함수컴포넌트에 적용시켜서 이해하면 되는데, 리액트의 함수컴포넌트에서의 side effect는 렌더링이아닌 외부 세계에 영향을 주는 어떠한 행위라고 말할 수 있겠다.

✅ 대표적으로 Data Fetching (외부세계에서 값을 가져오기 때문), DOM에 직접 접근(돔은 브라우저에 있고 우리는 컴포넌트 세계에 있기 때문), setInterval과 같은 구독(외부 세계의 무언가를 계속 지켜보고 있기 때문에)등이 sideEffect라고 할 수 있겠다.

🛑 주의! onClick onChange는 sideEffect가 아니다. 컴포넌트 안에 있기 때문.

useEffect를 사용하는 경우는 크게 두 가지로 나누어 볼 수 있다. 하나는 사용 후 정리가 필요한 것과 정리가 필요하지 않은 것이다.


useEffect, 일단 찍어보자 콘솔에!!

다음과 같은 컴포넌트가 있다고 해보자. App 컴포넌트 본문에 바로 console.log('minju') 를 하나 찍고, useEffect(() ⇒ {console.log(’hello’)}를 찍어보았다.

😉 그렇다면 실행순서는?

1) 콘솔에 minju가 찍힘

2) 화면에 UI가 렌더링 됨

3) 콘솔에 hello가 찍힘

→ 바로 이게 기본 컨셉이다. 원래는 위에서부터 줄줄줄 읽어내려오기 때문에 내가 작성한 순서대로 실행되어야 하지만, useEffect는 기본적으로 모든게 rendering되고 나서 실행된다.

import React, { useEffect } from "react";
import Card from "./Card";
import contacts from "../contacts";

function App() {

  useEffect(() => {
    console.log("hello!");
  });

  console.log("minju");
  
  return (
    <div>
      <h1 className="heading">My Contacts</h1>
		</div>)
}


흑기사 역할을 해주는 useEffect

useEffect를 이해하기 가장 쉬운 예는 Data를 Fetching해오는 상황이다. 로그인을 하여 인스타그램에 딱! 들어갔을때, 초기에 데이터를 불러와야 할 것이다. 그렇다면 불러오는 함수를 어디에다가 작성해야 할까?

import React, { useState, useEffect } from "react";

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

	fetch(() => { DATA 불러오기})

	return(
		<>
			<h1> Count : {count} </h1>
			<button onClick={() => setCount(count+1)}> + </button>
		</>
	)
}

🛑 위와 같이 본문에 바로 작성했을 때의 문제점!!

→ 문제 1 : rendering을 막는다(block)

데이터가 진~~짜 많으면 렌더링이 될때까지 시간이 오래걸릴 것이다. (위에서 막혀서 밑에까지 못읽어..) 사용자가 내 사이트에 들어왔는데 화면에 아무것도 없으니까, 엥? 이게 뭐지 하면서 사이트를 이탈하게 되겠지....

→ 문제 2 : 매 렌더링마다 실행된다.

나는 데이터 한번만 불러오고 싶은데😭 button으로 카운트 하나씩 올릴 때마다, 즉 state가 변할 때마다, 다시 말해 “화면이 렌더링"될 때마다!! 자꾸 데이터 통신이 반복될 것이다.

👨‍🏭 이 때 흑기사처럼 등장하는 useEffect!

바디에 떡하니 있는 fetch함수를 useEffect로 감싼다.

useEffect(() => { fetch( DATA 불러다줘) }) 로 변경한다.

이렇게 되면 문제점 1은 해결할 수있다. 기본적인 화면 렌더링이 일어나고 내 데이터가 화면에 나온다. 따라서 첫 번째 문제점이었던 rendering을 막는 걸 해결했다. 그러나 여전히, 매 렌더링마다 데이터가 계속 불러와진다. 이걸 해결하려면? [] dependency array를 사용한다.

useEffect(() => { fetch( DATA 불러다줘) }, [] ) 로 변경한다.

의존성 배열은 useEffect가 실행되어야 하는 조건을 제시한다. 지금과 같이 괄호 안에 아무것도 없으면([ ])화면이 렌더링 되고 나서 한 번만! 실행해줘! 라는 의미가 된다. 괄호 안에 어떠한 state를 넣으면([state]) 그 state가 변경될 때만 useEffect를 실행해줘! 라는 의미가 된다.

코드로 정리하자면,

function myComponent() {
	useEffect(() => { dataFetching })
	return <div> hello {state} </div>
}
//위의 경우 렌더링을 막지는 않으나, state 변화될 때마다 매번 fetching을 날린다.
//이럴 때 사용하는게 바로 dependency array!
//array안에 있는 값이 바뀔때만 이펙트를 실행한다.

useEffect(dataFetching(),[]);
// 다음과 같이 변경해주면 첫 렌더링 시에만 dataFetching함수를 실행해준다.

useEffect(dataFetching(), [count]);
// 이제는 count 변화될 때만 함수 실행!

🤓 헷갈리는 상황 1 : 의존성 배열 변경될때만 실행하라고 했는데 왜 첫 렌더링시에 한번 실행되나?

아래 상황에서 실행 순서는?

첫 렌더링 시 : 콘솔에 hello 찍힘 → 화면에 UI 렌더링 됨 → 콘솔에 minju 찍힘

count값 변경시 : 콘솔에 hello찍힘 → 화면에 UI 렌더링 됨 → 콘솔에 minju찍힘

🧤 첫 렌더링 시에 ‘count’라는 애가 UI로 등장했기 때문에 한 번 찍힌다. 당황하지 말자!!

import React, { useState, useEffect } from "react";

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

	console.log('hello');
	
	useEffect(() => {console.log('minju')},[count])

	return(
		<>
			<h1> Count : {count} </h1>
			<button onClick={() => setCount(count+1)}> + </button>
		</>
	)
}

🤓 헷갈리는 상황 2 : 의존성 배열..! 배열이니까 두 개 값도 넣을 수 있다. 그렇다면, useEffect는 언제 실행될까?

useEffect(() => { console.log(quiz) }, [ count, input ])

위 처럼 작성하면 언제 실행될까?

  1. 둘 다 한번에 바뀌었을때
  2. 둘 중 하나라도 변경되었을 때

정답은..! ⇒ 둘 중 하나라도 변경되면 실행된다!


효과를 다 사용했다면 Clean-up으로 치워주자!!

🤫 구독 해제로 살펴보기

유튜브도 우리가 구독했다가 더이상 보기 싫으면 구독해제를 해버린다. 프로그래밍도 똑같다. 더 이상 구독할 필요가 없으면 잘 해제를 해줘야 한다. 이벤트리스너 같은 경우도 그렇다. 내가 한번 달았다가, 더 이상 그 이벤트 리스너가 필요없으면 치워줘야 한다. 아니면 불필요한..찌꺼기..찝찝한 잔재가 남는다. 이 때 효과를 해제하려면 return 해주면 된다.

useEffect ( () => { 
	setInterval( (console.log("1 second") => {}, 1000 )
}, [])

이 페이지를 불러왔을 때, 1초마다 콘솔에 1 second를 찍어주는 함수를 실행했다. 내가 이 페이지에서 할 일을 끝내고 다른 페이지로 갔는데에도 콘솔을 열어보면, 계속해서 콘솔에 1 second가 미친듯이 찍히고 있는 것을 볼 수 있다. 한번 구독 등록해 놓았기 때문이다 😅 이 경우에 치워주지 않으면 불필요하게 콘솔이 계속해서 쌓여가고 내 프로그램 성능은 계속 떨어지고.. 자! 그래서 이런 경우에 clean up이 매우 필요해진다.

해당 함수를 치워주는 함수를 return문에 작성해주면 해결!

useEffect ( () => { 
	setInterval( (console.log("1 second") => {}, 1000 );
	return () => {clearInterval()};
}, [count])

// 조금 더 가독성 있게 작성해보자면, 아래와 같이 함수를 분리해주고 useEffect가 잘 보이게 해줄수 있다.

function printSecondChange() {
	setInterval((console.log("1 sec") => {}, 1000);
	return () => { clearInterval() };
}

useEffect(printSecondChange, [count])

🤓 이벤트리스너로 살펴보기

나는 카운터 컴포넌트가 렌더링 되었을 때에, 바디 태그에 스크롤이벤트를 붙이고 싶다. 단순하게 생각했을 때, 아래와 같이 작성할 것이다.

const Counter = () => {
	useEffect( () => {
		document.body.addEventListener('scroll', console.log('scroll'))
	},[])
}

다른 페이지로 넘어갔다. 그런데도 스크롤할때마다 콘솔에 미친듯이 scroll이 찍힌다.. 해결해주려면?

const Counter = () => {
	useEffect( () => {
		document.body.addEventListener('scroll', console.log('scroll'));
		return () => {document.body.removeEventListner('scroll', console.log('scroll')}
	},[])
}

자, 이 페이지가 넘어가면 더이상 스크롤 이벤트가 실행되어도 아무 일이 일어나지 않도록 잘 정리해 주었다. (이제 내 콘솔은 깨끗해 😛 뿌듯!)

✅ Clean up이 있을때의 실행 순서는? “세탁기를 생각하자”

바로 위에서 본 이벤트리스너 붙였다가 떼는 코드에서의 실행순서는?

화면 렌더링 → 스크롤 붙이기 → 무언가 state가 변경되어 화면 재 렌더링 → 스크롤 떼기 → 스크롤붙이기

함수가 적힌 순서대로 ‘state변경시 렌더링 → 스크롤붙이기 → 떼기’로 생각하면 안된다. 기본적인 동작방식은, return문 안에 보면 콜백함수로 적혀있는 것을 볼 수있다. 즉 바로 실행하지 말라는 뜻인데, react가 이 두 번째 클린업 함수를 기억하고 있다가, 다시 그 함수가 실행될때 먼저 clean up 함수를 실행 후 의도한 함수를 실행한다.

세탁기에 세탁물이 있을 때, 우리는 세탁물을 꺼내고 새로운 세탁물을 넣는다. 동일한 원리라고 생각하면 된다.


공식 문서에서 알려 주는 몇 가지 Q&A

Q1. useEffect가 하는 일은?

⇒ useEffect라는 훅을 이용하여 React에게 컴포넌트가 렌더링 이후 해야핼 일을 말해준다. 리액트는 우리가 넘긴 함수(effect)를 기억했다가 DOM 업데이트 후 이 함수를 불러낸다.

Q2. useEffect를 컴포넌트 안에서 불러내는 이유는?

⇒ 컴포넌트 내부에 둠으로써 effect를 통해 컴포넌트 내부에서 선언한 state변수에 접근할 수 있게 되기 때문이다. 함수 범위 안에 존재하기 때문에 접근 가능하며, 자바스크립트의 클로저 개념을 생각하면 된다.

Q3. useEffect는 렌더링 이후에 매번 수행되는 것인가?

그렇다. 기본적으로 첫번째 렌더링 + 이후의 모든 업데이트에서 수행된다고 생각하면 된다.

Q4. 💫 useEffect에 전달된 함수가 모든 렌더링에서 다른가?

그렇다. 이는 의존하고 있는 state값이 제대로 업데이트 되었는지에 대한 걱정없이 effect 내부에서 그 값을 읽을 수 있게 하려고 했기 때문이다. 리렌더링 하는 때마다 모두 이전과 다른 effect로 교체하여 전달한다.


마무리하며

새로운 개념을 마주하는 자세는, 두려움이 아니라 호기심이어야 한다. 이걸 왜 만들었을까? 왜 필요할까? 관점에서 바라보면 새로운 개념은 날 편하게 해 주기 위해 등장한 것이라는 것을 깨닫는다. 그리고 그 개념 혹은 기능을 만들어준 분께 감사하게 된다🤩. 새로운 것을 공부해가는 과정에서, 하나를 이해했다고 생각했는데 또 하나가 나올 때 순간 당황스러울 때가 참 많다. 그럴 때에는 기억하자. 내가 곧 마주하게 될 당황스러운 문제상황을 누군가가 해결해 놓은 것이고, 나는 누군가의 수고로움을 얻어타고 그 문제를 잘 해결해 낼 수 있을테니 애초에 문제가 뭐였는지 한번 살펴보자고!

좋은 웹페이지 즐겨찾기