사이드 이펙트는 한국어로는 '부작용'이라는 뜻
예를 들면 감기약을 먹었을 때
감기 증상은 없어졌지만 (작용)
피부가 붉게 올라오면 (부작용)
이 약에는 부작용이 있다고 할 수 있다.
프로그래밍에선 말 그대로 외부에 부수적인 작용을 하는 걸 말한다.
let count = 0;
function add(a, b) {
const result = a + b;
count += 1; // 함수 외부의 값을 변경
return result;
}
const val1 = add(1, 2);
const val2 = add(-4, 5);
위 코드에서 add 함수를 보면
이 함수는 a, b 를 파라미터로 받아서 더한 값을 리턴한다.함수 코드 중에서 함수 바깥에 있는
count 라는 변수의 값을 변경하는 코드가 있다.
add 함수는 실행하면서 함수 외부의 상태(count 변수)가 바뀌기 때문에,
이런 함수를 "사이드 이펙트가 있다"고 한다.
익숙한 예로는 console.log 함수를 예로 들 수 있다.
console.log 함수를 사용하면 값을 계산해서 리턴하는 게 아니라웹 브라우저 콘솔 창에 문자열을 출력하는데
외부 상태를 변경해서 문자열을 출력하는 것이다.
이렇게 함수 안에서 함수 바깥에 있는 값이나 상태를 변경하는 갓을
'사이드 이펙트'라고 부른다.
useEffect 는 리액트 컴포넌트 함수 안에서
사이드 이펙트를 실행하고 싶을 때 사용하는 함수
ex)
DOM 노드를 직접 변경하거나
브라우저에 데이터를 저장하고,
네트워크 리퀘스트를 보내는 것
주로 리액트 외부에 있는 데이터나 상태를 변경할 때 사용
useEffect(() => {
document.title = title; // 페이지 데이터를 변경
}, [title]);
useEffect(() => {
fetch('https://example.com/data') // 외부로 네트워크 리퀘스트
.then((response) => response.json())
.then((body) => setData(body));
}, [])
useEffect(() => {
localStorage.setItem('theme', theme); // 로컬 스토리지에 테마 정보를 저장
}, [theme]);
참고: localStorage 는 웹 브라우저에서 데이터를 저장할 수 있는 기능
useEffect(() => {
const timerId = setInterval(() => {
setSecond((prevSecond) => prevSecond + 1);
}, 1000); // 1초마다 콜백 함수를 실행하는 타이머 시작
return () => {
clearInterval(timerId);
}
}, []);
참고: setInterval 이라는 함수를 쓰면 일정한 시간마다 콜백 함수를 실행할 수 있다.
여기서 한 가지 의문점이 들 수도 있다.
앞에서 우리는 데이터 불러오는 기능 만들 때처음에는 onclick 이벤트 핸들러에서 네트워크 리퀘스트를 보냈고,
페이지가 열리자마자 데이터를 불러오기 위해서
useEffect 를 사용하는 걸로 바꿨다.
하지만 만약 둘 다 가능한 경우라면 핸들러 함수를 써야 하는 걸까?
아니면 useEffect 를 써야 하는 걸까?
여기에 정해진 규칙은 없다.
하지만 useEffect 는 쉽게 말해서 '동기화'에 쓰면 유용한 경우가 많다.
여기서 동기화는 컴포넌트 안에 데이터와
리액트 바깥에 있는 데이터를 일치시키는 것을 말한다.
이게 어떤 의미인지 간단한 예시를 통해 살펴보자.
아래 App 컴포넌트는 인풋 입력에 따라 페이지 제목을 바꾸는 컴포넌트이다.
import { useState } from 'react';
const INITIAL_TITLE = 'Untitled';
function App() {
const [title, setTitle] = useState(INITIAL_TITLE);
const handleChange = (e) => {
const nextTitle = e.target.value;
setTitle(nextTitle);
document.title = nextTitle;
};
const handleClearClick = () => {
const nextTitle = INITIAL_TITLE;
setTitle(nextTitle);
document.title = nextTitle;
};
return (
<div>
<input value={title} onChange={handleChange} />
<button onClick={handleClearClick}>초기화</button>
</div>
);
}
export default App;
handleChange 함수와 handleClearClick 함수가 있다.
모두 title 스테이트를 변경한 후에 document.title 도 함께 변경해주고 있다.
여기서 document.title 값을 바꾸는 건 외부의 상태를 변경하는 거니까
사이드 이펙트이다.
import { useEffect, useState } from 'react';
const INITIAL_TITLE = 'Untitled';
function App() {
const [title, setTitle] = useState(INITIAL_TITLE);
const handleChange = (e) => {
const nextTitle = e.target.value;
setTitle(nextTitle);
};
const handleClearClick = () => {
setTitle(INITIAL_TITLE);
};
useEffect(() => {
document.title = title;
}, [title]);
return (
<div>
<input value={title} onChange={handleChange} />
<button onClick={handleClearClick}>초기화</button>
</div>
);
}
export default App;
useEffect 를 사용한 예시에서는 document 를 다루는 사이드 이펙트 부분만 따로 처리하고 있다.
이렇게 하니 setTitle 함수를 쓸 때마다 document.title 을 변경하는 코드를 신경 쓰지 않아도 되니까 편리하다.
게다가 처음 렌더링 되었을 때 'Untitled'라고 페이지 제목을 변경하는 효과까지 낼 수 있다.
그리고 이 코드를 본 사람이라면 누구든 이 컴포넌트는 title 스테이트 값을 가지고 항상 document.title 에 반영해줄 것이라고 쉽게 예측할 수 있다.
이렇게 useEffect 는 리액트 안과 밖의 데이터를 일치시키는데 활용하면 좋다.
useEffect 를 사용했을 때 반복되는 코드를 줄이고,
동작을 쉽게 예측할 수 있는 코드를 작성할 수 있기 때문이다.
useEffect(() => {
// 사이드 이펙트
return () => {
// 사이드 이펙트에 대한 정리
}
}, [dep1, dep2, dep3, ...]);
useEffect 의 콜백 함수에서 사이드 이펙트를 만들면 정리가 필요한 경우가 있다.
이럴 때 콜백 함수에서 리턴 값으로 정리하는 함수를 리턴할 수 있었다.
리턴한 정리 함수에서는 사이드 이펙트에 대한 뒷정리를 한다.
예를 들면 이미지 파일 미리보기를 구현할 때 Object URL을 만들어서 브라우저의 메모리를 할당(createObjectURL) 했는데요. 정리 함수에서는 이때 할당한 메모리를 다시 해제(revokeObjectURL)해줬었다.
콜백을 한 번 실행했으면, 정리 함수도 반드시 한 번 실행된다
정확히는 새로운 콜백 함수가 호출되기 전에 실행되거나
(앞에서 실행한 콜백의 사이드 이펙트를 정리),컴포넌트가 화면에서 사라지기 전에 실행된다
(맨 마지막으로 실행한 콜백의 사이드 이펙트를 정리).
import { useEffect, useState } from 'react';
function Timer() {
const [second, setSecond] = useState(0);
useEffect(() => {
const timerId = setInterval(() => {
console.log('타이머 실행중 ... ');
setSecond((prevSecond) => prevSecond + 1);
}, 1000);
console.log('타이머 시작 🏁');
return () => {
clearInterval(timerId);
console.log('타이머 멈춤 ✋');
};
}, []);
return <div>{second}</div>;
}
function App() {
const [show, setShow] = useState(false);
const handleShowClick = () => setShow(true);
const handleHideClick = () => setShow(false);
return (
<div>
{show && <Timer />}
<button onClick={handleShowClick}>보이기</button>
<button onClick={handleHideClick}>감추기</button>
</div>
);
}
export default App;
일정한 시간 간격마다 콜백 함수를
실행하는 setInterval 이라는 함수도 정리가 필요한 사이드 이펙트이다.
매번 타이머를 시작하기만 하면 타이머가 엄청나게 많아진다.
이 컴포넌트는 렌더링이 끝나면 타이머를 시작하고,
화면에서 사라지면 타이머를 멈춘다.
코드 정리해보기
사용자가 '보이기' 버튼을 눌렀을 때 show 값이 참으로 바뀌면서 다시 렌더링 된다.
조건부 렌더링에 의해서 Timer 컴포넌트를 렌더링 한다.
Timer 컴포넌트에서는 useEffect 에서 타이머를 시작하고, 정리 함수를 리턴한다.
콘솔에는 '타이머 시작 🏁'이 출력
다시 사용자가 '감추기' 버튼을 누르면 show 값이 거짓으로 바뀌면서 다시 렌더링
조건부 렌더링에 의해서 이제 Timer 컴포넌트를 렌더링하지 않는다.
그럼 리액트에선 마지막으로 앞에서 기억해뒀던 정리 함수를 실행한다.
타이머를 멈추고 콘솔에는 '타이머 멈춤 ✋'이 출력
이런 식으로 정리 함수를 리턴하면
사이드 이펙트를 정리하고 안전하게 사용할 수 있다는 것.