useEffect는 클래스 컴포넌트에서 Lifecycle 메서드를 통해 복잡하게 다뤘던 Side Effect 로직을 개선합니다.
컴포넌트의 마운트, 업데이트, 언마운트 등의 Lifecycle을 하나의 useEffect hook으로 처리하기 때문입니다.
이를 통해 함수형 컴포넌트에서도 쉽게 Side Effect를 다룰 수 있습니다.
하지만 useEffect의 철학은 단순히 Lifecycle을 대체하는 것이 전부가 아닙니다.
"thinking in effects."
useEffect의 철학은 Side Effect를 Lifecycle이 아닌, 관심사에 따라 관리할 수 있게 하는 것입니다.
(Dan Abramov의 A Complete Guide to useEffect 내용이 많은 도움 되었습니다.)
등장 배경
// Class Component
class Example extends React.Component {
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
}
useEffect 등장 이전의 클래스 컴포넌트는 Lifecycle 메서드를 이용해 Side Effect를 처리합니다.
간단한 Side Effect 처리에도 복잡한 코드 작성이 필요했기 때문에 유지보수가 어렵습니다.
// Functional Component
const Example = () => {
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
}
이러한 문제를 해결하고자 useEffect가 등장합니다.
useEffect는 Side Effect를 Lifecycle이 아닌, 관심사에 따라 관리할 수 있게 합니다.
useEffect란?
useEffect는 컴포넌트가 렌더링 될 때마다 Side Effect 로직을 다루는 hook입니다.
기존 클래스 컴포넌트에서 렌더링과 관련된 Lifecycle 메서드는 다음과 같습니다.
- componentDidMount : 컴포넌트가 마운트 될 때
- componentDidUpdate : 컴포넌트가 업데이트 될 때
- componentWillUnmount : 컴포넌트가 언마운트 될 때
useEffect는 위의 Lifecycle 메서드들을 하나의 hook으로 관리합니다.
이를 통해 함수형 컴포넌트에서도 쉽게 Side Effect를 다룰 수 있습니다.
Side Effect란?
본래 Side Effect는 함수가 실행되면서 함수 외부의 값이나 상태를 변경시키는 것을 의미합니다.
리액트의 함수형 컴포넌트는 props와 state를 바탕으로 컴포넌트를 렌더링 합니다.
만약 컴포넌트의 렌더링과 무관한 연산들이 존재한다면 이들이 Side Effect가 됩니다.
무관한 연산을 컴포넌트 내에서 직접 수행하는 것은 옳지 않은 개발 방식입니다.
왜냐하면 개발자가 컴포넌트의 렌더링을 통제할 수 없기 때문입니다.
비동기를 비롯한 대표적인 Side Effect 예시는 아래와 같습니다.
- 데이터를 가져오기 위해서 외부 API를 호출할 때
- 네트워크를 통해 Request를 전송할 때
- setTimeout(), setInterval() 등의 타이머 함수를 사용할 때
- 직접 컴포넌트의 DOM을 수정할 때...
useEffect 사용법
🔎 deps란?
useEffect(function, deps)
useEffect의 두 번째 인자인 deps(dependencis)는 의존성 배열 매개변수를 의미합니다.
useEffect는 이 매개변수에 전달된 값을 토대로 Side Effect 로직을 실행합니다.
1. deps가 없을 때
useEffect(() => {
console.log("컴포넌트가 렌더링 될 때 마다 실행");
})
배열을 생략하면 Side Effect 로직은 컴포넌트가 렌더링 될 때마다 실행됩니다.
모든 렌더링에 대응하기 때문에, 의도와 다르게 Side Effect가 실행될 수 있습니다.
되도록 deps를 비우는 것은 지양하는 것이 좋습니다.
2. deps가 빈 배열일 때
useEffect(() => {
console.log("최초 렌더링 이후에 한번만 실행");
}, [])
deps에 빈 배열을 전달하면 Side Effect 로직은 컴포넌트 최초 렌더링 이후 한 번만 실행됩니다.
빈 배열은 주로 아래와 같은 상황에서 사용합니다.
- DOM을 사용해야 하는 외부 라이브러리를 연동할 때
- axios, fetch 등을 통해 서버에 데이터를 요청할 때
- DOM의 속성을 읽거나 변경할 때...
3. deps에 특정 props, state가 담겼을 때
useEffect(() => {
console.log(name);
}, [name])
deps에 특정 props, state를 전달하면 Side Effect 로직은 컴포넌트 최초 렌더링 및 해당 값이 변경될 때마다 실행됩니다.
4. 클린업 함수
useEffect(() => {
console.log("componentDidUpdate");
return () => {
console.log('clean up');
};
}, [name])
useEffect에 return문이 존재하면 이는 컴포넌트가 언마운트 됐을 때 실행되는 클린업 함수가 됩니다.
주로 아래와 같은 상황에서 사용합니다.
- DOM 이벤트를 제거할 때
- clearTimeout을 통한 타이머 함수를 종료할 때
- 소켓을 닫을 때...
Lifecycle
useEffect와 관련 있는 Lifecycle 메서드는 다음과 같습니다.
- componentDidMount : 컴포넌트가 마운트 될 때
- componentDidUpdate : 컴포넌트가 업데이트 될 때
- componentWillUnmount : 컴포넌트가 언마운트 될 때
useEffect는 이 세 개의 Lifecycle에서 전부 실행됩니다.
🔎 componentDidMount
componentDidMount() {
console.log("componentDidMount");
}
componentDidMount 메서드는 컴포넌트가 마운트 된 직후 호출됩니다.
deps에 빈 배열을 전달했을 때가 이 메서드와 대응됩니다.
🔎 componentDidUpdate
componentDidUpdate(prevProps, prevState) {
if (prevProps.value !== this.props.value) {
console.log("componentDidUpdate");
}
}
componentDidUpdate 메서드는 컴포넌트가 업데이트된 직후 호출됩니다.
deps가 없거나, 특정 props나 state가 담겼을 때가 이 메서드와 대응됩니다.
🔎 componentWillUnmount
componentWillUnmount(){
console.log("componentWillUnmount");
}
componentWillUnmount 메서드는 컴포넌트가 언마운트되기 직전 호출됩니다.
useEffect의 클린업 함수가 이 메서드와 대응됩니다.
useEffect 관련 팁
💡 useEffect를 통한 관심사의 분리
클래스 컴포넌트는 Lifecycle을 중심으로 Side Effect를 모으다 보니 관련 없는 로직들이 섞이는 문제가 생깁니다.
반면 useEffect는 Lifecycle에 따라서가 아닌 관심사에 따라서 Side Effect 로직을 나눌 수 있습니다.
그래서 되도록 하나의 거대한 useEffect로 모든 Side Effect를 관리하지 않는 것이 좋습니다.
Side Effect의 관심사에 따라 개별적인 useEffect들로 분리하도록 노력해봅시다.
관심사의 분리
- 프로그램의 여러 부문으로 나눠 각자의 관심사를 가지도록 설계하는 원칙
💡 렌더링 이후에 useEffect가 실행되는 이유
리액트는 컴포넌트가 렌더링된 이후에 useEffect를 실행합니다.
만약 실행 과정에서 state가 변경된다면 한번 더 렌더링이 발생합니다.
렌더링 이전에 useEffect를 실행하면 한 번에 처리될 것을 굳이 왜 두 번의 렌더링 과정을 걸칠까요?
결론적으로 렌더링 이전에 Side Effect를 처리하는 것은 리액트 성능에 좋지 않습니다.
가상 돔 렌더 과정에 Side Effect가 포함되면 Side Effect가 발생할 때마다 가상 돔을 다시 렌더 해야 하기 때문입니다.
게다가 일부 Side Effect는 서버의 응답을 받아야만 다음 과정을 진행하는 경우도 있습니다.
이러한 문제 때문에 가상 돔은 Side Effect를 제외한 순수한 컴포넌트만을 사용해 렌더링을 하고 있습니다.
그래야만 리액트의 핵심인 재조정 알고리즘을 효율적으로 실행할 수 있기 때문입니다.
게다가 렌더링 이후에 비동기의 Side Effect를 처리하는 것은 사용자 경험에도 좋습니다.
만약 서버와의 통신에 문제가 있어도 그 영향을 최소화할 수 있기 때문입니다.
그래서 시간이 많이 걸리거나, 비동기 작업을 처리할 때 useEffect가 유용합니다.
💡 useLayoutEffect
대부분의 Side Effect는 useEffect를 통한 비동기 처리가 권장됩니다.
하지만 DOM의 업데이트로 인해 발생하는 화면 깜빡임은 사용자 경험에 좋지 않습니다.
이러한 상황을 만났을 때 useLayoutEffect가 좋은 해결책이 될 수 있습니다.
useEffect
useEffect는 컴포넌트가 render(계산)와 paint(그리기)까지 된 후에 비동기적으로 실행됩니다.
그래서 useEffect 내부에 DOM에 영향을 주는 코드가 있다면 화면 깜빡임이 생길 수 있습니다.
useLayoutEffect
useLayoutEffect는 컴포넌트가 render(계산)된 후에 동기적으로 실행됩니다.
그 후 마지막으로 paint(그리기)가 실행됩니다.
그래서 useLayoutEffect 내부에 DOM에 영향을 주는 코드가 있어도 화면 깜빡임이 생기지 않습니다.
짧은 내용의 글이지만 생각보다 엄청 많은 시간과 노력이 들었습니다..
useEffect의 철학을 온전히 이해하여 정리하는 과정이 어려웠기 때문입니다.
만약 비슷한 문제를 충분히 경험하고 고민해봤다면 더욱 이해하기 수월했을 것 같습니다.
앞으로도 이 글은 지속적으로 수정하고 보완할 생각입니다.
틀린 부분 알려주시면 감사하겠습니다!
'프론트엔드 > React' 카테고리의 다른 글
[React] SOLID 원칙을 컴포넌트에 적용하기 (0) | 2022.06.28 |
---|---|
[React] useState의 동작 원리와 클로저 (2) | 2022.05.20 |
[React] map()에 key를 사용하는 이유 (index를 key로 쓰면 안되는 이유) (0) | 2022.05.17 |
[React] 엘리먼트와 컴포넌트 (Element, Component, JSX) (0) | 2022.05.17 |