
useEffect란 component가 렌더링 될 때마다 실행할 작업을 선택하고 실행하도록 하는 Hook이다.
component가 mount/unmount/update 됐을 때, 특정 작업을 실행할 수 있다.
우리가 useEffect를 사용해야 하는 이유는 component가 최대한 순수 함수가 될 수 있도록 하기 위함이다. component가 순수 함수가 되기 위해서는 side effect를 따로 관리하여야 하고, 이는 component가 렌더링 될 때 특정 조건에 의존하여 수행되도록 하여야 한다. 리액트는 component가 최대한 순수 함수를 유지할 것을 권장하며, 실제로도 component 재사용성을 증가시키고 예측 및 테스트를 쉽게 하도록 하는 중요한 요인이다. 이를 도와주는 Hook이 useEffect이다.
간단하게 설명하자면, useEffect는 외부 세계와 상호작용하면서 해당 컴포넌트의 렌더링이나 성능에는 영향을 미치지 않도록 만들어 주는 도구이다.
그렇다면 순수 함수, Side Effect란 무엇일까?
순수 함수(pure function)란, 외부와 관련이 없으며 함수 외부에 영향을 주지 않는 함수를 말한다.
즉, 같은 입력에 대해 항상 같은 결과를 반환해주는 함수이다.
const App({input}) {
return (<div>{input}</div>);
};
위 예시와 같은 component는 input이라는 인자를 받고, div를 반환하는 함수이다. 여기서 input은 props라고 부르며, App은 component라고 부른다. 이때 App이라는 함수는 같은 props를 전달하면 항상 같은 결과를 반환하므로 순수 함수이다.
이처럼 외부와 전혀 관련이 없으며 함수 외부에 영향을 주지 않는(side effect가 없는) 함수를 순수 함수라고 하며, 참조에 투명하다고 한다.
만약 함수가 같은 입력임에도 다른 결과를 내거나, 함수 외부에 영향을 미치는 경우 side effect가 있다고 하며 참조에 불투명하다고 한다.
Side Effect란, component가 렌더링 된 이후에 비동기로 처리되어야 하는 부수적인 효과들이다.
React에서는 component 외부에 도달해야 하는 경우 side effect를 수행하며, 외부 세계와 함께 수행되기 때문에 예측할 수 없다.
대표적인 side effect의 예시로는
일반적으로 데이터를 가져오기 위해 API를 요청하는 경우, 화면에 렌더링할 수 있는 것들을 먼저 렌더링하고 실제 데이터는 비동기로 가져오는 것이 권장된다. 요청 즉시 1차 렌더링을 함으로써 API 응답이 늦어지거나 응답이 없을 경우에도 영향을 최소화 시킬 수 있기 때문이다. 이때 실제 데이터를 가져오는 부분이 side effect인 것이다.
side effect에는 두 종류가 있다.
- clean-up이 필요한 경우
- clean-up이 필요하지 않은 경우 (network request, DOM 수동조작 등)
그렇다면 clean-up이란 무엇일까? (파고파도 나오는 이론 공부...)
React에서 clean-up 함수는 useEffect Hook에서 return되는 함수이다.
우리는 때로 side effect를 멈춰야 하는 경우가 있다. 예를 들어 setInterval을 사용하여 state를 설정해였을 경우, side effect를 clean-up 하지 않을 경우 component가 unmount되고 더이상 사용하지 않을 때 state는 소멸되지만 setInterval 함수는 계속 실행된다. 이렇게 되면 setInterval은 존재하지 않는 state를 업데이트하려고 할 것이다. 이것은 메모리 누수의 원인이 된다. 이때 clean-up을 사용하여 setInterval을 중지할 수 있다.
clean-up은 이처럼 update 직전(특정 값이 변경되기 직전) / unmount 이전(component가 사라지기 이전)에 실행할 작업을 지정할 수 있다.
즉, 아래와 같은 순서로 동작한다.
component unmount ➡️ clean-up 함수 실행 ➡️ component mount ➡️ effect 실행
밑에 자세한 예시를 추가할 것이지만, 간단한 예시를 보면 아래와 같이 사용한다.
useEffect(() => {
// mount 시점, deps update 시점에 실행할 작업
return () => {
// unmount 시점, deps update 직전에 실행할 작업
};
}, [deps]);
마지막으로 clean-up을 하는 이유를 간단하게 정리하면
이론은 여기까지 알아보고 이제 useEffect의 동작 방식과 사용 방법을 알아보자~ 🔥
useEffect가 하는 일을 간단하게 설명하면,
React에게 component가 렌더링 이후에 어떤 일을 수행하는지 전달하는 것이다. 우리가 넘긴 함수를 기억했다가 update를 수행한 이후에 실행하도록 한다.
위의 그림을 통해 확인할 수 있지만,
useEffect(function, deps)
기본 형태
useEffect(() => {
mount 시 실행할 함수;
return () => {unmount 시 실행할 내용}
}, [의존성 배열 deps]);
- mount 시 실행할 함수 작성
- return 값에 unmount 시 실행할 내용 작성(clean-up)
- deps 배열 작성
deps 배열에 특정 값을 넣으면, 해당 값이 업데이트 될 때마다 함수가 재실행된다.
이때, 빈 배열이라도 작성해야 useEffect에 작성한 함수가 처음 mount 될 때만 실행된다. 생략할 경우 리렌더링 될 때마다 계속 함수를 불러온다.
deps가 빈 배열일 때
deps가 빈 배열이 아닐 때 (검사하고자 하는 값을 넣었을 때)
예제로 설명하면,
deps가 빈 배열일 때 => mount 될 때만 실행된다.
useEffect(() =>{
console.log('mount 될 때만 실행');
}, []);
deps를 생략했을 때 => 렌더링 될 때마다 실행된다.
useEffect(() =>{
console.log('리렌더링 될 때마다 실행');
});
deps가 검사할 값을 넣었을 때 => 특정값이 업데이트 될 때마다 실행된다.
useEffect(() =>{
console.log(input);
console.log('업데이트 때마다 실행');
}, [input]);
즉, useEffect 내에서 사용하는 상태 혹은 props가 있는 경우에 deps에 넣어주어야 한다.
위에서 setInterval을 사용하여 state를 설정해였을 경우, side effect를 clean-up 하지 않을 경우 component가 unmount되고 더이상 사용하지 않을 때 state는 소멸되지만 setInterval 함수는 계속 실행된다는 예시를 들었다.
function Timer() {
const [time, setTime] = useState(0);
useEffect(() => {
setInterval(() => setTime(1), 1000);
}, []);
}
위 코드와 같은 경우, component가 unmount되더라도 setInterval이 계속 업데이트를 시도하고 메모리 누수의 원인이 될 것이다.
function Timer() {
const [time, setTime] = useState(0);
useEffect(() => {
let interval = setInterval(() => setTime(1), 1000);
return () => {
// unmount 시점에 clean-up 함수 실행
clearInterval(interval);
}
}, []);
}
위와 같이 수정할 경우, unmount 이후 clean-up 함수가 실행되고 setInterval이 지워져 계속 state를 업데이트 하려는 오류가 발생하지 않을 것이다.
useEffect와 useLayoutEffect의 차이점을 알기 위해선 먼저 다음의 두 가지 개념을 알고 있어야 한다.
- Render: DOM Tree를 구성하기 위해 각 엘리먼트 스타일 속성을 계산하는 과정
- Paint: 실제 스크린에 Layout을 표시하고 업데이트하는 과정
두 개의 가장 큰 차이를 설명하기 위해 대표적으로 사용되는 라이프사이클 이미지로 비교해보고자 한다.
위 이미지에서 알 수 있듯이, useEffect는 components가 render와 paint된 후 비동기적으로 실행된다. paint 이후 실행되기 때문에, useEffect에 DOM에 영향을 주는 코드가 있을 경우 화면의 깜빡임을 보게 된다. 이는 value가 업데이트 될 때 잠깐 0이었다가 값이 셋팅되는 것이다.
useLayoutEffect는 components가 render 된 후 실행되고 이후에 paint가 동기적으로 실행된다. paint 이전에 실행되기 때문에, DOM에 영향을 주는 코드가 있더라도 화면의 깜빡임은 없다. value가 업데이트 되더라도, useLayoutEffect가 paint보다 먼저 실행되기 때문에 0이 보이기 전에 값을 셋팅한다.
useLayoutEffect는 렌더링 전에 특정 행동을 수행하도록 만들어주므로, 애니메이션 구현 등 즉시 반응이 필요한 경우와 성능 모니터링 기능을 사용할 때 등의 경우에 유용하다. (크롬 브라우저의 DevTools에서는 layout과 paint 성능을 측정할 수 있으며, 이러한 성능 모니터링 기능을 사용 시 useLayoutEffect를 사용하면 유용)
하지만 useLayoutEffect는 렌더링 이전에 특정 작업을 수행하므로 브라우저의 렌더링 파이프라인에 큰 영향을 미칠 수 있다는 단점이 있다. 때문에 성능 문제가 발생할 때만 useLayoutEffect를 사용하고 기본적으로는 useEffect를 사용하는 것이 좋다.
useRef란 저장공간 또는 DOM 요소에 접근하기 위해 사용되는 Hook이다.
이때 Ref는 reference를 의미한다.
- useRef는 속성을 변경해도 리렌더링하지 않는다. component의 속성만 조회/수정한다.
- component가 리렌더링되어도 component가 unmount 되기 전까지는 값을 유지할 수 있다.
위와 같은 성질 때문에, state에 담긴 값이 변경될 때마다 리렌더링이 일어나지 않기 때문에 성능이 향상된다는 장점이 있다.
const 변수명 = useRef(초기값) // 초기값을 적어 생성
<input ref= {변수명}/> // 반환요소 접근
useRef를 사용할 때 주의점은 결과값이 {current:초기값}을 가진 객체가 반환되며, current라는 키값을 지닌 프로퍼티가 생성되므로 값을 변경하기 위해서는 current에 접근해야 한다.
function App() {
const [count, setCount] = useState(1);
const [renderingCount, setRedneringCount] = useState(1);
useEffect(() => {
console.log("rendering Count : ", renderingCount);
setRedneringCount(renderingCount + 1);
});
return (
<div>
<div>Count : {count}</div>
<button onClick={() => setCount(count + 1)}> count up </button>
</div>
);
}
만약 위와 같이 코드를 작성한다면, useEffect에 의해서 계속 component를 리렌더링하기 때문에 무한 루프에 빠지게 된다.
function App() {
const [count, setCount] = useState(1);
const renderingCount = useRef(1);
useEffect(() => {
console.log("renderingCount : ", renderingCount.current);
++renderingCount.current;
});
return (
<div>
<div>Count : {count}</div>
<button onClick={() => setCount(count + 1)}> count up </button>
</div>
);
}
하지만 위와 같이 useRef를 이용한다면 렌더링 횟수를 추적할 수 있다.
useEffect는 이용해본 적 있지만, useLayoutEffect는 사용해본 경험이 없는데 이번 기회에 차이를 확실하게 알게 되었다. useEffect도 deps가 업데이트 될 때마다 실행된다는 것 정도만 알고 사용하고 있었는데 deps를 생략하면 렌더링 될 때마다 실행된다는 사실을 다시 한 번 되짚어보았다.
자꾸 너무 자세히 정리하려고 하는 것 같다... 나 혼자 공부하기엔 좋지만 읽을 때 지루할 수 있을 것 같으니 요점만 쓸 수 있게 노력해야지!! 하지만 내가 기초적인 부분이 부족해서 남들도 알았으면 하는 마음에 자꾸자꾸 양이 늘어나는 건 어쩔 수 없는 것 같다,,,
참고한 자료
react useEffect 공식 문서
F-Lab 리액트에서의 useEffect 이해와 활용
useEffect와 useLayoutEffect 관련 Medium blog
라이프사이클 이미지 출처
useRef 예제 코드 출처
아티클을 읽으면서 궁금했던 부분을 언급해주면서 꼬리 물기 식으로 설명해주신 부분이 좋았습니다. 저도 궁금했던 부분인데, 따로 찾아보지 않고 바로바로 정보를 습득할 수 있어서 편했습니다. 각 훅 별로 사용 방법을 이해하기 편하게 예시와 함께 설명해주어서 더욱 유익했습니다. 특히, 마지막에 몰랐던 부분과 더 공부하고 싶은 부분 / 공부하면서 든 생각까지 작성해주어 리뷰 하는 모습에서 깊이 있게 주제에 대해서 공부하였다는게 느껴져서 감명 깊었습니다. 좋은 글 감사합니다!
교과서 같이 잘 정리된 아티클 잘 읽었습니다!! useEffect 함수를 이해하기 위해 필요한 개념들이 모두 잘 정리 되어 있어서 아티클을 읽으면서 따로 해당 개념들을 찾아보지 않아도 되서 너무 좋았습니다! useEffect에 대해 처음 공부하거나 완전하게 이해하고 싶은 분들한테 추천해주고 싶을 만한 아티클인거 같아요~~ 또 내용이 꽤 긴 편인데도 필력도 좋고 정리도 깔끔하게 되어있어서 굉장히 매끄럽게 읽었습니다! 좋은 아티클 너무 잘 읽고갑니다 수고하셨어요!!!!!
useEffect 훅을 사용해야 하는 이유가 side effect를 따로 관리해야 하기 위해서라는 건 알고 있었는데 순수함수가 될 수 있게 하기 위함이었군요! 꼼꼼한 글 덕분에 읽으면서 저절로 이론이 정리되는 기분이 드네요
이번 프로젝트에서 크롬을 통해 (충격적인)성능 측정을 한 적이 있는데, useLayoutEffect를 통해 성능 개선을 할 수 있겠어요!
너무 자세하게 정리하려고 하는 것 같다고 써주셨는데 그만큼 너무 유용한 아티클이었어요! 고생하셨어요☺️
같은 내용으로도 이렇게 자세하고 읽기 쉽게 썼다는게 신기한것 같아요! 단순히 글 설명 뿐만 아니라 useEffect, useLayoutEffect의 실행 순서를 이미지로 보여주는 부분이 훨씬 이해가 쉽게 되는것 같습니다. 감사합니다!
하나의 이론만 정리하신게 아니라 그에 파생되는 이론들까지 정리해주셔서 해당 개념들에 대해 더욱 깊게 이해할 수 있었습니다 !! 순수 함수나 side effect 개념은 명확히 모르고 있었는데 덕분에 따로 찾아보지 않고서도 이해할 수 있었습니다. 자세히 정리해주신 것이 정말 좋았어요 ㅎㅎ
체계적으로 잘 정리한 아티클 감사합니다 👍
오오 넘 도움이 많이되는 글입니다 <3 공부는 역시 디테일하게 하는 게 제맛이죵! ㅎㅎ 저도 기초적인 부분부터 취약한 부분이 너무너무 많아서 정말 유익하게 읽었습니다!!
개념에 대한 설명도 자세하게 해주시고 예시, 사진들이 있어서 더욱 이해가 잘 됐어요 더 공부해보고 싶은 내용두 파이팅 입니다~!~!!