우선 useEffect를 살펴보기 전에 우리는 생명 주기(Life Cycle)
에 대해 알 필요가 있는데요.
생명 주기란 컴포넌트가 활성화 되는 기간, 즉 컴포넌트의 생성(create) - 수정(update) - 종료(delete)
를 포괄한 개념인데요.
위의 사진에서도 알 수 있듯이, 컴포넌트는 최초 생성될 때 돔 트리(DOM Tree)에 마운트(Mounte) 되고, 업데이트를 거쳐 리렌더링 된 뒤(Update) 돔 트리에서 언마운트(Unmounted)되는 과정을 거치는데요.
이처럼 리액트의 모든 컴포넌트는 해당 그림의 라이프 사이클 패턴을 가지고 있는 셈이죠.
위에서 살펴본 바와 같이, 컴포넌트는 기본적으로 생명 주기를 가지는데요. 이때 마운트, 업데이트, 언마운트가 될 때 추가적인 작업 서버 데이터 접근 및 상태 표시 등의 작업을 하고 싶을 때 사용되는 방법으로 존재하는 것이 바로 컴포넌트 생명주기 메서드
라고 합니다.
참고로 생명 주기 간의 마운ㅌ, 업데이트, 언마운트 과정은 각각 Rendering(가상 DOM 업데이트)
작업과 Commiting(실제 DOM에 적용)
하는 작업으로 나뉘어 집니다. 간단히 생각해서 Rendering은 렌더링을 위한 준비 작업, Commiting은 실제 렌더링을 반영하는 작업 정도로 이해하시면 되는데요.
아무튼 이러한 생명 주기 메서드는 클래스형 컴포넌트 생명주기 메서드와 함수형 컴포넌트 생명주기 메서드
가 존재하지만, 훅 등장 이전까지 사용되었던 클래스형 컴포넌트 생명주기 메서드에 비해 훅 이후 등장한 함수형 컴포넌트 생명주기 메서드가 코드의 양으로나 가독성으로보나 더 뛰어나
기 때문에 이번 섹션에서는 리액트에서 주로 사용되는 메서드인 useEffect
에 대해서 살펴보겠습니다.
- 클래스형 컴포넌트
(자료 출처 : https://kim-mj.tistory.com/250)
- 함수형 컴포넌트
useEffect 훅
은 처음부터 생명 주기를 염두에 두고 설계된 훅이 아니라고 합니다. 이 말의 의미는 컴포넌트가 CUD(create-update-delete)
되는 시점마다 부수 효과(side effect)
를 주기 위해 설계된 훅(Hooks)
이라고 하는데요.
근데 이게 리액트 컴포넌트의 생명 주기 관리에 성격이 잘 맞아 사용되게 되었으며, 결과적으로는 훅의 등장 이후에 옮겨진 컴포넌트의 생성 양상 (클래스형 -> 함수형)에 맞춰 생명 주기 관리 또한 useEffect 훅을 사용
하게 된 것이죠.
그럼 우선 useEffect의 간단한 골자부터 살펴보겠습니다.
useEffect(()=>{}, [props,state])
useEffect의 매개변수
로는 콜백 함수
와 의존성 배열
이 들어가는데요. 콜백 함수에는 의존성 배열의 요소에 변화가 생길 때 실행할 코드
를 의미하고, 의존성 배열에는 useEffect에서 변화를 추적할 상태나 속성값을 지정
하게 되어있습니다.
좀 더 구체적으로 코드를 작성하면 다음과 같은데요.
function Component(){ useEffect(() = > { console.log('val is updated'); }, [val]) }
위 컴포넌트에서 val 이라는 스테이트 값을 넣었는데요. 위 코드의 의미는 val의 값이 업데이트 될 때 마다 'val is updated' 문구를 출력하라는 의미가 되겠죠.
그럼 저 작업은 특정 요소에 대한 update시 발생되는 부수 효과 생성인데, 마운트(create)와 언마운트시(delete) 발생되는 부수 효과
는 어떻게 생성할까요? 바로 빈 배열, 즉 의존성 값을 전달해주지 않으면
됩니다.
function Component(){ useEffect(() = > { console.log('component is mounted'); return () => { console.log('Component unmounted'); }; }, []) }
위의 코드는 빈 배열을 전달했을 때 실행할 마운트, 언마운트에 대한 부수 효과를 정의한 코드인데요.
useEffect의 콜백 함수가 실행될 때와 반환할 때(render)
를 나누어 각각 마운트와 언마운트 시점의 부수 효과를 생성하는 방식으로 정의할 수 있는 것이죠.
그럼 다음 코드 예시를 통해 useEffect의 쓰임새에 대해 알아보겠습니다.
function Component(){ useEffect(() = > { console.log('val is updated'); }, [val]) }
좀 더 명확하게 알아보기 위해 간단한 카운터 앱을 만들고, 해당 예제에서 useEffect를 사용해 보겠습니다.
import { useState, useEffect, useRef } from 'react'; function Counter() { // useState를 사용하여 count 상태를 선언합니다. const [count, setCount] = useState(0); const isMount = useRef(false); // 컴포넌트가 마운트 되었을 때 실행되는 useEffect useEffect(() => { console.log('Component mounted'); }, []); useEffect(() => { if (!isMount.current) { isMount.current = true; return; } console.log(`Component has updated and count is ${count}`) }), [count]; useEffect(() => { return () => { console.log('Component unmounted'); }; }); const onClickButton = (val) => { setCount(count + val); }; return ( <div> <p>Count: {count}</p> <button onClick={() => onClickButton(1)}>Increment</button> <button onClick={() => onClickButton(-1)}>Decrement</button> </div> ); } export default Counter;
일단 가장 먼저 해줘야 하는 작업은 useEffect 훅을 임포트 하는 작업입니다. 그 후 마운트 될 때 useEffect를 지정해 주는데요.
useEffect(() => { console.log('Component mounted'); }, []);
위 코드에서는 최초로 컴포넌트가 마운트 될 때 해당 문구를 출력하기 위해 빈 배열을 두번째 매개변수로 전달해 주었습니다. 이렇게 코드를 작성하면 해당 컴포넌트가 마운트 되는 시점에 딱 한번만 코드가 출력
됩니다.
useEffect(() => { console.log(`Component has updated and count is ${count}`) }), [count];
두번째 코드는 컴포넌트가 업데이트 될 때, 즉 의존성 배열의 count 값이 변경될 때마다 해당 문구를 출력하는 예제인데요. 이때 의존성 배열의 값이 하나일 경우 배열을 생략해도 됩니다.
useEffect(() => { return () => { console.log('Component unmounted'); }; });
세번째 코드는 return문에 문구를 작성해 컴포넌트가 언마운트 될 때 해당 문구를 출력하도록 구현한 예제인데요. 이때 구현되는 대표적인 함수로는 클린업 함수 (데이터 정리) 가 있습니다.
자 그럼 이렇게 구현되어 있는 예제를 이용해 최초로 컴포넌트를 생성해 볼까요?
하지만 출력된 콘솔 내용을 보면 뭔가 이상합니다. 분명 마운트는 딱 한번 실행이 된다고 하는데 두 번 실행되었고, 버튼 또한 누르지 않았는데 마찬가지로 두 번 표시되고 있습니다.
일단 그 이유를 설명해 보자면, mount가 두 번 출력된 이유는 App 컴포넌트에 StrictMode가 적용되었기 때문인데요. 물론 이는 개발자 빌드 단계에서만 발생 되는 현상으로, 실제 빌드 단계에서 한 번만 출력되는게 맞습니다.
일단 StrictMode가 발생되는 이유는 다음 블로그를 참고해 보시면 좋을 것 같고, 그렇다고 이게 문제가 되는건 아닙니다. 그리 크게 신경 쓸 일도 아니죠.
아무튼 Mount가 두 번 발생되는 이유는 다음과 같고요.
그렇다면 Component has updated and count is 0
는 왜 두 번 생성이 되는 것인가? 이것도 StrictMode의 영향 때문인데요.
앞서 소개한 StrictMode에 의한 코드 출력은 총 2번이 발생되는데, 이를 방지하는 방법은 StrictMode를 삭제하는 방법도 존재하나 조건문을 삽입해서 해당 코드가 실행되지 않고 반환문으로 종료를 하게 하는 방법
도 존재합니다. (참고로 예제에서 사용된 useRef의 사용법은 해당 섹션에서 확인할 수 있습니다.)
//useRef객체를 false값으로 생성 후 isMount에 할당 const isMount = useRef(false); useEffect(() => { //컴포넌트가 최초 마운팅 될 때 실행을 막기 위해 참 조건 실행문으로 isMount의 현재값을 true로 변환 후 리턴. 이럴 경우 최초 마운트 시 리턴에 의해 isMount 값은 true로 변환되고, 이 후 if의 참 조건에 걸리지 않아 그 밑의 콘솔값이 출력됩니다. if (!isMount.current) { isMount.current = true; return; } console.log(`Component has updated and count is ${count}`) }), [count];
그럼 위처럼 수정된 코드를 실행할 때 다음과 같이 콘솔에 출력되는데요.
이 상태에서 버튼을 여러번 눌러보겠습니다.
정상 작동 되는 것을 알 수 있는데, 여기서 주의할 점은 마운트
는 컴포넌트가 돔에 적용 될 최초의 시점에서 한 번만 발생하지만 이후 재등록 시점부터는 마운트가 발생되지 않는 반면, 언마운트
는 최초부터 삭제될 때 마다 발생한다는 것입니다.