1. 정의
1.1. 일반적 정의
함수 내의 어떤 부분이 외부에 영향을 미치거나 영향을 받을 때, side effect(부수 효과)가 있다고 일컫는다.
side effect가 없는 함수를 pure function(순수 함수)라 부르며, 그렇지 않은 함수를 비순수 함수라 부른다.
즉 순수 함수가 되기 위해서는 함수 실행에 의해 함수 외부의 요소를 변화시켜서는 안 되며, 인자로 들어온 값을 제외한 외부 요인에 의해 함수의 실행결과가 달라져서도 안 된다.
다시 말해 동일한 input이 주어졌을 때, 동일한 output의 결과를 내야 하며, 외부 요인에 영향을 받거나 미치지 않아야 순수 함수의 조건을 만족시킨다.
다음은 side effect가 있는 함수의 예시이다.
let name = 'YU';
const nPureFunc = () => {
name = 'KIM';
};
nPureFunc(); // 함수 실행에 의해 함수 외부에 있는 전역변수의 값이 변경되므로 부수효과가 있음
const rand = () => {
return Math.random();
};
const value = rand(); // 함수를 실행할 때마다 출력되는 값이 매번 달라지기 때문에 pure 하지 않다
const getServerData = () => {
axios.get("url").then()...
};
getServerData(); // 서버의 상태에 따라 응답 결과가 달라질 수 있기 때문에 pure하지 않음
let num = 3;
const add = (a, b) => {
return a + b + num;
}
const result = add(1, 2); // 외부 요인 num에 의해 함수 실행 결과가 달라지므로 순수하지 않다.
다음은 pure한 함수의 예시이다.
const add = (a, b) => {
return a + b;
}
const result = add(1, 3); // 주어진 input에 대해 항상 동일한 결과를 반환하므로 pure하다.
const upper = (str) => {
return str.toUpperCase();
}
const str = upper('hello');
1.2. React에서의 정의
간단하게 말해 React가 제공하는 기능 이외의 모든 행위를 다 side effect가 있다고 일컫는다.
- DOM 직접 조작하기 (이벤트 설정 포함)
- fetch, axios, socket 등을 통해 외부와 통신하기
- local storage, session storage를 통해 데이터를 저장하거나 불러오기
- setTimeout, setInterval과 같은 타이밍 함수 사용하기
React의 목적은 (MVC 패턴의 관점에서) view의 역할을 수행하는 것이기 때문에, 상기한 요소들은 엄밀히 말해 React의 주 목적과는 큰 관련이 없다.
또한 이런 행위들은 React의 랜더링 과정에 영향을 미칠 수 있기 때문에, side effect가 있는 행위를 useEffect를 통해 따로 분리하지 않는다면 일관되지 않은 렌더링 결과를 마주칠 가능성이 생긴다. (유지보수하기 어려운 코드가 된다)
side effect를 제대로 분리하지 않았을 때 발생하는 대표적인 경우는 아래와 같다.
- 매 렌더링마다 불필요하게 side effect가 수행된다.
- side effect에 의해 렌더링이 지연된다.
예를 들어 다음과 같이 두 가지의 코드를 작성했다.
(무거운 작업을 실행하는 side effect 코드가 컴포넌트 코드 본문에 바로 있는지, 혹은 useEffect 내부에 있는지의 차이)
(hard 함수는 무거운 작업이 동반되는 side effect 코드라고 생각해 주시길)
import { useState } from "react";
const hard = () => {
for (let i = 0; i < 200000; i++) {
console.log(i);
}
};
const App = () => {
const [count, setCount] = useState(0);
hard(); // 대충 무거운 작업이라고 가정
const handleClick = () => {
setCount((prev) => prev + 1);
};
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>click</button>
</div>
);
};
export default App;
import { useEffect, useState } from "react";
const hard = () => {
for (let i = 0; i < 200000; i++) {
console.log(i);
}
};
const App = () => {
const [count, setCount] = useState(0);
useEffect(() => {
hard(); // 대충 무거운 작업이라고 가정
}, []);
const handleClick = () => {
setCount((prev) => prev + 1);
};
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>click</button>
</div>
);
};
export default App;
무거운 side effect 함수(이하 hard함수)가 컴포넌트 본문에 그대로 위치해 있을 경우, 렌더링이 진행되는 과정에 hard 함수의 실행도 포함되어있기 때문에, 렌더링이 매우 오래걸린다.
hard 함수의 위치에 따라 FCP에 어떠한 차이가 발생했는지 실험을 해본 결과, hard 함수가 useEffect 내부에 있는 경우의 FCP는 약 8180ms가 걸린 반면, hard 함수가 컴포넌트 본문에 위치한 경우 FCP는 16100ms로 거의 2배가량 차이가 발생했다.
게다가 위의 컴포넌트는 click 이벤트가 발생했을 때 state를 변경시키므로 리랜더링이 발생하게 되는데, hard 함수가 useEffect 내부에 위치해있지 않을 경우 랜더링 될 때마다 hard 함수가 실행되기 때문에, 결과적으로 엄청나게 느린 웹이 탄생하게 된다.
3. 결론
React 컴포넌트 내부에서 작성하는 코드가 side effect인지 파악할 필요성이 있다.
side effect인 경우 useEffect 내부에 작성함으로서 코드를 분리시켜야 하며, side effect가 아닌 코드만 구성되어있을 경우 불필요하게 useEffect를 호출하는 일이 없어야 할 것이다.