위코드 파운데이션 과정을 들으며 정리한 내용입니다.
아래처럼 함수의 return 문 위에서 Side Effect 를 실행하면, Side Effect 가 렌더링을 블락킹할 수 있습니다.
const App = () => {
const doSideEffect = () => {
// do some side effect
};
doSideEffect();
// 여기서 사이드 이펙트 실행!
return <h1>Hello World</h1>
};
코드는 위에서 아래로 순차적으로 실행하므로, 사이드 이펙트 doSideEffect() 가 끝날 때까지 JSX 를 리턴하는 코드로 넘어가지 않습니다. 사이드 이펙트의 동작이 복잡하다면, 화면 UI 를 바로 렌더링 하지 못해, 사용자에게 안 좋은 경험을 줄 수 있습니다.
두번째로 매번 수행될 필요가 없는 사이드 이펙트가 매 렌더링마다 수행되는 것도 좋지 않습니다.
const App = () ={
// 코드 생략
getFeeds();
// data fetching side effect
return 피드리스트;
}
이 상황에서 화면에 그려지는 피드리스트에 좋아요 기능을 추가한다면, 좋아요를 클릭할 때마다 리렌더링을 실행하게 됩니다. 위와 같이 데이터를 받아오는 데이터 패칭에 대한 사이드 이펙트를 함수 본문에 넣어놨다면, 사소한 리 렌더링이 발생할 때마다 매번 데이터를 패칭하게 됩니다. (비효율적)
위 두가지 문제점을 개선하기 위해 useEffect를 사용합니다. 렌더링을 블락킹하지 않아야하고, 매 렌더링 마다 조건부로 실행을 콘트롤할 수 있어야 합니다.
리액트에서 useEffect 함수를 import 하고 함수를 인자로 받는 useEffect 함수를 실행합니다. 이 때 인짜로 사용한 콜백함수는 렌더링이 완료된 후(JSX 리턴 후) 실행됩니다.
import React, { useEffect } from "react";
function App() {
console.log("side effect");
const printConsole = () => {
console.log("side effect with useEffect");
};
useEffect(printConsole);
return <h1>Hello wecode</h1>;
}
export default App;
이제 매번 렌더링 되는 것을 해결하는 방법을 살펴봅니다. 아래처럼 setCount 나 setText 를 통해 count 나 text 상태값이 변할 때 리 렌더링이 진행되는 코드가 있다면, useEffect 를 사용해도 블락킹만 막고, 리 렌더링을 막지는 못합니다.
import React, { useEffect, useState } from "react";
function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
useEffect(() => {console.log("change!")}); // 콜백함수 식을 바로 써도 됨
return (
<div>
<div className="wrapper">
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>Up</button>
</div>
<input value={text}> onChange={(e)=>{setText(e.target.value)}} />
</div>
);
}
export default App;
이를 해결하기 위해서 useEffect 에 의존성 배열이라는 두번째 인자를 넣어주어야 합니다. 첫번째 렌더링에서는 무조건 콜백함수를 실행하지만 두번째 렌더링 부터는 의존성 배열의 값이 변해야 콜백함수를 호출합니다.
useEffect(()=>{
console.log("count changed")
}, [count]);
위 코드는 count 값이 변했을 때만 콜백함수를 호출하고,
useEffect(()=>{
console.log("all changed")
}, [count, text]);
위 코드는 count 와 text, 두가지 값중 하나면 변해도 이펙트를 실행합니다.
useEffect(()=>{
console.log("only first render");
}, []);
이렇게 빈 배열을 넣어야 첫번째 실행 이후 콜백함수가 호출되지 않습니다. (변화를 감지할 값이 없으므로) 이펙트 렌더링 사이클을 한번 돌아보면,
1/ 컴포넌트 렌더링, 최초 렌더링 - mount
2/ useEffect 첫 번째 인자로 넘겨준 콜백 함수 호출 - Side Effect
3/ 컴포넌트의 state 또는 props 가 변경되면 리렌더링 발생 - update
4/ useEffect 두 번째 인자에 들어이는 의존성 배열 확인 - 상태값 변화를 감지하면 콜백함수 실행
5/ state 또는 props 가 변경되면 3-4 과정 반복
6/ 컴포넌트가 더 이상 필요없어지면 (화면에서 사라지면) - unmount
클린업은 불필요하게 남아있는 사이드 이펙트를 정리하고 치워줍니다.
useEffect(()={
console.log("Hello World");
});
위와 같이 이후 코드 동작에 영향을 주지 않는 이펙트는 클린업이 필요없지만,
useEffect(()=>{
setInterval(()=>{
console.log("interval");
}, 100);
});
이렇게 setInterval 메소드로 100ms 마다 콘솔을 찍어주는 이펙트는 다른 페이지로 이동해도 불필요하게 동작할 수 있습니다. 이런 동작들은 클린업해야 합니다. 아래 코드는 이펙트에 의존성 배열을 전달하지 않아서 매번 렌더링 될 때마다 이펙트가 발생해 이벤트 리스터를 추가하게 됩니다.
useEffect(()={
const button = document.getElementById("consoleButton");
const printConsole = () => {
console.log("button clicked");
};
button.addEventListener("click", printConsole);
});
다음 이펙트가 발생하기 전에 기존에 부착한 이벤트리스너를 제거하고 새롭게 이펙트를 발생시키기 위해 클린업을 한다면, 아래처럼 동작하고 싶은 것을 함수형태로 만들어서 마지막에 리턴하면 됩니다. 이러면 컴포넌트가 언마운트 될 때도 클린업 함수를 실행해줍니다.
useEffect(()={
const button = document.getElementById("consoleButton");
const printConsole = () => {
console.log("button clicked");
};
button.addEventListener("click", printConsole);
return () => {
button.removeEventListener("click", paintConsole);
};
});
또 다른 예시입니다.
useEffect(() => {
console.log("effect");
const timerID = setInterval(()=>{
console.log("interval");
}, 1000);
return () => {
clearInterval(timerId); // setInterval 해제하는 함수 사용
};
});
클린업은 불필요한 이펙트를 정리하고 치워줍니다. 클린업할 함수를 해제하는 함수를 리턴문에 넣어서 실행해주면 됩니다.