useEffect, 단어를 뜯어보면 효과를 사용한다는 것이다. 그런데 여기서 말하는 효과는 무슨 효과를 사용하는 것이지? 🤔 바로 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를 사용하는 경우는 크게 두 가지로 나누어 볼 수 있다. 하나는 사용 후 정리가 필요한 것과 정리가 필요하지 않은 것이다.
다음과 같은 컴포넌트가 있다고 해보자. 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를 이해하기 가장 쉬운 예는 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가 변할 때마다, 다시 말해 “화면이 렌더링"될 때마다!! 자꾸 데이터 통신이 반복될 것이다.
바디에 떡하니 있는 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 변화될 때만 함수 실행!
아래 상황에서 실행 순서는?
첫 렌더링 시 : 콘솔에 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>
</>
)
}
useEffect(() => { console.log(quiz) }, [ count, input ])
위 처럼 작성하면 언제 실행될까?
정답은..! ⇒ 둘 중 하나라도 변경되면 실행된다!
유튜브도 우리가 구독했다가 더이상 보기 싫으면 구독해제를 해버린다. 프로그래밍도 똑같다. 더 이상 구독할 필요가 없으면 잘 해제를 해줘야 한다. 이벤트리스너 같은 경우도 그렇다. 내가 한번 달았다가, 더 이상 그 이벤트 리스너가 필요없으면 치워줘야 한다. 아니면 불필요한..찌꺼기..찝찝한 잔재가 남는다. 이 때 효과를 해제하려면 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')}
},[])
}
자, 이 페이지가 넘어가면 더이상 스크롤 이벤트가 실행되어도 아무 일이 일어나지 않도록 잘 정리해 주었다. (이제 내 콘솔은 깨끗해 😛 뿌듯!)
바로 위에서 본 이벤트리스너 붙였다가 떼는 코드에서의 실행순서는?
화면 렌더링 → 스크롤 붙이기 → 무언가 state가 변경되어 화면 재 렌더링 → 스크롤 떼기
→ 스크롤붙이기
함수가 적힌 순서대로 ‘state변경시 렌더링 → 스크롤붙이기 → 떼기’로 생각하면 안된다. 기본적인 동작방식은, return문 안에 보면 콜백함수로 적혀있는 것을 볼 수있다. 즉 바로 실행하지 말라는 뜻인데, react가 이 두 번째 클린업 함수를 기억하고 있다가, 다시 그 함수가 실행될때 먼저 clean up 함수를 실행 후
의도한 함수를 실행한다.
세탁기에 세탁물이 있을 때, 우리는 세탁물을 꺼내고 새로운 세탁물을 넣는다. 동일한 원리라고 생각하면 된다.
Q1. useEffect가 하는 일은?
⇒ useEffect라는 훅을 이용하여 React에게 컴포넌트가 렌더링 이후 해야핼 일을 말해준다. 리액트는 우리가 넘긴 함수(effect)를 기억했다가 DOM 업데이트 후 이 함수를 불러낸다.
Q2. useEffect를 컴포넌트 안에서 불러내는 이유는?
⇒ 컴포넌트 내부에 둠으로써 effect를 통해 컴포넌트 내부에서 선언한 state변수에 접근할 수 있게 되기 때문이다. 함수 범위 안에 존재하기 때문에 접근 가능하며, 자바스크립트의 클로저 개념을 생각하면 된다.
Q3. useEffect는 렌더링 이후에 매번 수행되는 것인가?
그렇다. 기본적으로 첫번째 렌더링 + 이후의 모든 업데이트에서 수행된다고 생각하면 된다.
Q4. 💫 useEffect에 전달된 함수가 모든 렌더링에서 다른가?
그렇다. 이는 의존하고 있는 state값이 제대로 업데이트 되었는지에 대한 걱정없이 effect 내부에서 그 값을 읽을 수 있게 하려고 했기 때문이다. 리렌더링 하는 때마다 모두 이전과 다른 effect로 교체하여 전달한다.
새로운 개념을 마주하는 자세는, 두려움이 아니라 호기심이어야 한다. 이걸 왜 만들었을까? 왜 필요할까? 관점에서 바라보면 새로운 개념은 날 편하게 해 주기 위해 등장한 것이라는 것을 깨닫는다. 그리고 그 개념 혹은 기능을 만들어준 분께 감사하게 된다🤩. 새로운 것을 공부해가는 과정에서, 하나를 이해했다고 생각했는데 또 하나가 나올 때 순간 당황스러울 때가 참 많다. 그럴 때에는 기억하자. 내가 곧 마주하게 될 당황스러운 문제상황을 누군가가 해결해 놓은 것이고, 나는 누군가의 수고로움을 얻어타고 그 문제를 잘 해결해 낼 수 있을테니 애초에 문제가 뭐였는지 한번 살펴보자고!