React
useEffect는 왜 useEffect 일까?
최근 코딩테스트 서술형 질문에서 useEffect 라는 훅은 왜 Effect라는 이름으로 사용했고, 함수형 컴포넌트에서는 클래스 기반 컴포넌트처럼 라이프 사이클 메서드를 지원하지 않고 useEffect라는 훅을 사용했는지에 대한 질문을 받고 혼란스러웠습니다.
훅을 어떻게 사용하는지에만 집중하였고, 그 배경과 원리에 대한 이해가 부족했던 것 같습니다.
그래서 이번 포스트에서는 useEffect 라는 훅에 대해서 정리해보려고 합니다.
왜 useEffect?
리액트(react) 공식 문서에 의하면 useEffect 는 외부 시스템과 컴포넌트를 동기화 할 수 있게 해주는 리액트 훅(hook)이라고 설명하고 있습니다. 여기서 설명하는 것은 훅의 주 용도에 대한 설명인 것 같습니다.
여기서 말하는 외부 시스템은 react에 의해 직접 제어 되지 않는 요소를 의미합니다.
- API 통신: 서버와 데이터를 주고 받는 작업
- DOM 제어: useRef를 이용해 특정 DOM 요소에 접근하고 조작하는 작업
- 전역 객체와의 상호작용: window와 같은 전역 객체에 이벤트 리스너를 등록하거나 해제하는 작업 이 외에도 여러 상황에서 useEffect를 사용하게 됩니다.
이렇게 컴포넌트(함수)가 입력 및 출력을 넘어서 다른 데이터를 조작할 때, 이러한 컴포넌트에는 Side Effect(부수 효과) 가 있다고 표현합니다.
만약 useEffect hook을 사용하지 않고 이러한 Side Effect를 컴포넌트 내에서 관리한다면, 상태(state)나 속성(props)의 변화가 있을 때마다 컴포넌트가 리렌더링되고 그 로직이 실행될 것입니다. 렌더링과 무관한 로직이 렌더링 과정에서 실행되면 성능에 악영향을 끼칠 수 있습니다.
따라서 React는 이러한 Side Effect를 관리하기 위한 적절한 장소로 useEffect hook을 제공합니다.
useEffect라는 이름도 이 설명에서 유추할 수 있습니다.
렌더링 이외에 사용해야하는 Side Effect를 관리하는 장소로서 useEffect라는 이름이 붙여진 것으로 보입니다!
라이프 사이클 메서드를 지원하지 않는 이유
함수형 컴포넌트에서 클래스형 컴포넌트와 다르게 라이프 사이클 메서드를 지원하지 않고 useEffect 훅을 사용하는 이유는 API의 단순화, 유연성, 함수형 프로그래밍의 장점, 정리 작업의 통합, 성능 최적화 등을 통해 부수 효과를 효과적으로 관리하기 위함입니다.
- 단순화된 API:
함수형 컴포넌트는 더 간결하고 이해하기 쉬운 API를 제공합니다. 클래스형 컴포넌트에서는 여러 라이프 사이클 메서드componentDidMountcomponentDidUpdatecomponentWillUnmount를 관리해야 하지만, useEffect는 이러한 여러 메서드를 하나의 훅으로 통합하여 관리할 수 있게 해줍니다. - 유연성:
useEffect는 컴포넌트의 상태나 props에 따라 언제 부수 효과를 실행할지를 세밀하게 조정할 수 있습니다. 의존성 배열을 통해 특정 상태나 props가 변경될 때만 효과가 실행되도록 설정할 수 있어, 더 유연한 상태 관리가 가능합니다. - 함수형 프로그래밍의 장점:
React는 함수형 프로그래밍 패러다임을 채택하고 있습니다. 함수형 컴포넌트와 useEffect를 사용하면, 부수 효과를 선언적으로 관리할 수 있어 가독성과 유지보수성을 향상시킬 수 있습니다. - 정리 작업의 통합
useEffect는 부수 효과와 그에 대한 정리(cleanup) 작업을 같은 곳에서 처리할 수 있도록 설계되었습니다. 이는 코드의 일관성을 높이고, 개발자가 라이프 사이클 관리에 필요한 모든 작업을 한 곳에서 처리할 수 있게 합니다. - 성능 최적화
함수형 컴포넌트는 불필요한 리렌더링을 줄이는 데 유리합니다. useEffect를 사용하면 부작용이 렌더링 과정에 영향을 미치지 않도록 관리할 수 있어, 성능을 더욱 최적화할 수 있습니다.
useEffect 사용하기
function Component() {
...
useEffect(() => {
// Side Effect
}, [state, props])
...
}
첫 번째 인수로는 실행할 부수 효과가 포함된 함수, 두 번째 인수로는 의존성 배열을 전달한다.
이 의존성 배열은 길이가 긴 배열일 수도, 아무런 값이 없는 빈 배열일 수도 있고, 배열 자체를 넣지 않고 생략할 수도 있다.
- 빈 배열일 경우
useEffect(() => {
console.log('render');
}, [])
비교할 의존성이 없다고 판단해 최초 렌더링 직후에 실행된 다음부터는 더 이상 실행되지 않습니다.
- 아무런 값도 넘겨주지 않을 경우
useEffect(() => {
console.log('render');
})
의존성을 비교할 필요 없이 렌더링할 때마다 실행이 필요하다고 판단해 렌더링이 발생할 때마다 실행됩니다.
(보통 컴포넌트가 렌더링됐는지 확인하기 위한 방법으로 사용됩니다.)
- 의존성 배열에 상태 또는 props를 추가할 경우
function Component() {
const [counter, setCounter] = useState(0);
function handleClick() {
setCounter((prev) => prev + 1);
}
useEffect(() => {
console.log('카운터: ', counter);
}, [counter])
return (
<>
<h1>{counter}</h1>
<button onClick={handleClick}>+</button>
</>
)
}
의존성에 상태를 추가하면 추가한 상태의 값이 변경될 때 useEffect 내부 부수 효과를 포함한 함수가 실행된다. 위 예제를 보면 button 을 클릭하여 counter 의 상태 값을 변경하면console.log('카운터: ', counter) 부분이 실행된다.
위 처럼 useEffect는 자바스크립트의 proxy나 데이터 바인딩, 옵저버 같은 특별한 기능을 통해 값의 변화를 관찰하는 것이 아닌, 렌더링할 떄마다 의존성에 있는 값을 보면서 이전과 다른 값이 하나라도 있으면 부수 효과를 실행하는 함수라 볼 수 있다.
클린업 함수
클린업 함수는 이전에 일으킨 Side Effect(부수 효과)를 정리할 때 사용합니다.
useEffect 첫 번째 인자인 callback 함수에서 함수를 리턴하여 사용할 수 있습니다.
일반적으로 알려져 있고, 실제로 자주 사용하는 이벤트 리스너 케이스로 예시를 살펴보겠습니다.
function Component() {
const [counter, setCounter] = useState(0);
function handleClick() {
setCounter((prev) => prev + 1);
}
useEffect(() => {
function mouseEvent() {
console.log(counter);
}
window.addEventListener('click', mouseEvent);
// 클린업 함수
return () => {
console.log('클린업 함수 실행됨', counter);
window.removeEventListener('click', mouseEvent);
}
}, [counter])
return (
<>
<h1>{counter}</h1>
<button onClick={handleClick}>+</button>
</>
)
}
위 컴포넌트를 실행하면 다음과 같은 로그를 볼 수 있습니다.
클린업 함수 실행됨 0
1
클린업 함수 실행됨 1
2
클린업 함수 실행됨 2
3
...
위 로그를 보면 클린업 함수는 생명주기 메서드의 언마운트 개념과는 차이가 있는 것을 확인할 수 있습니다.
componentWillUnmount: 특정 컴포넌트가 DOM에서 사라진다는 것을 의미하는 클래스형 컴포넌트의 메서드입니다. 클린업 함수는 언마운트라기 보다는 함수형 컴포넌트가 리렌더링됐을 때 의존성 변화가 있었을 당시 이전 값을 기준으로 실행된다 즉, 이전 상태를 청소해 주는 개념으로 보는 것이 맞습니다.
마무리하며
코딩테스트는 아쉽게도 좋은 결과를 얻지 못했습니다. 시험 당시의 환경이나 컨디션 때문이 아니라, 저의 준비가 매우 부족했던 탓이었습니다. 기억에 남는 질문 중 하나인 useEffect에 대해 제대로 답하지 못한 것이 매우 아쉬웠습니다. 그래서 다시 공부할 겸 이렇게 포스팅을 하며 복기해봤습니다.
이번 과정을 통해 useEffect의 사용법뿐만 아니라 그 배경과 원리를 이해하게 되니, 앞으로 이 훅을 더욱 적재적소에 활용할 수 있다는 자신감이 조금이나마 생겼습니다. 이번 코딩 테스트 경험을 통해 저 자신을 되돌아볼 수 있는 시간을 가졌고, 좌절하지 않고 좋은 경험으로 삼아 앞으로 나아가겠다는 다짐을 하게 되었습니다. 다시 한 번 해당 질문에 대한 답변을 정리하며 마무리하겠습니다.
Q. useEffect 라는 훅은 왜 Effect라는 이름으로 사용했고, 함수형 컴포넌트에서는 클래스 기반 컴포넌트처럼 라이프 사이클 메서드를 지원하지 않고 useEffect라는 훅을 사용했나요?
A. useEffect hook은 컴포넌트의 렌더링 이외의 Side Effect(부수 효과)를 관리하는 역할을 합니다. "Effect"라는 이름은 이러한 부작용을 처리하는 기능을 강조하기 위한 것입니다.
함수형 컴포넌트는 클래스 기반 컴포넌트보다 더 간결한 API를 제공하며, 복잡한 라이프 사이클 메서드를 사용하지 않고도 부작용을 관리할 수 있게 해줍니다. useEffect를 사용함으로써 Side Effect(부수 효과)를 선언적으로 관리할 수 있어, 코드의 가독성과 유지보수성이 향상됩니다. 이러한 방식은 React의 함수형 프로그래밍 패러다임과 잘 어울리며, 개발자가 부작용을 쉽게 이해하고 처리할 수 있도록 도움을 주기 때문입니다.