The Lifecycle of React Hooks Component 글의 내용을 번역한 것입니다.
이 글에서는 useEffect 콜백과 cleanUp이 일어나는 순서를 알아보겠습니다.
또한 앱이 마운트, 마운트 해제, 업데이트되는 각 상황이 어떻게 다른지 하나의 코드 예시를 통해 확인해보려 합니다.
useEffect hook의 동작 순서에 대한 내용이니 익숙하신 분은 스킵하셔도 됩니다 :)
모든 컴포넌트에는 세 단계가 존재합니다.
1. Mount
2. Update
3. Unmount
이 단계는 컴포넌트가 처음으로 페이지에 마운트되는 단계입니다. 이 단계에서 hook의 흐름은 다음과 같습니다.
Lazy initializers 실행
게으른 초기화(lazy initializaers)
라고 합니다. 게으른 초기화 함수는 오직 첫 마운트 단계에서만 실행되고, 이 후에 다시 리렌더링이 된다면 이 함수의 실행은 무시됩니다.렌더링
useState
hook과 다른 값들이 표현되는 때입니다.리액트의 DOM 업데이트
Layout Effects 실행
브라우저의 스크린 그리기
Effects 실행
이 단계는 컴포넌트가 업데이트되는 단계입니다. 이 업데이트는 다음과 같은 이유들로 발생합니다:
이 단계에서 hook의 흐름은 다음과 같습니다.
렌더링
리액트의 DOM 업데이트
Layout Effects Cleanup 실행
useLayoutEffect
도 useEffect
처럼 클린업 단계가 존재합니다.Layout Effects 실행
브라우저의 스크린 그리기
Effects Cleanup 실행
Effects 실행
보시다시피, 마운트 단계에서 과정과 유사하지만, Layout Effects와 Effects Cleanup
이 실행된다는 것이 차이점입니다.
이 단계는 컴포넌트가 페이지로부터 언마운트되는 단계입니다. 이 단계에서 hook의 흐름은 다음과 같습니다.
Layout Effects Cleanup 실행
Effects 실행
이 단계에서는 Cleanup만이 실행됩니다.
예시를 보기 전에, useEffect
콜백의 세 가지 다른 유형을 살펴봅시다.
useEffect(() => {
console.log('useEffect(() => {})') // Line 1
return () => {
console.log('useEffect(() => {}) cleanup') // Line 2
}
})
이 useEffect
는 의존성이 없습니다.
useEffect(() => {
console.log('useEffect(() => {}, [])') // Line 1
return () => {
console.log('useEffect(() => {}, []) cleanup') // Line 2
}
}, [])
이 useEffect
는 의존성 배열을 받지만 해당 배열이 비어있습니다.
Note: 이 useEffect
콜백은 빈 의존성 배열을 가지므로 컴포넌트가 업데이트될 때는 실행되지 않습니다.
useEffect(() => {
console.log('useEffect(() => {}, [count])') // Line 1
return () => {
console.log('useEffect(() => {}, [count]) cleanup') // Line 2
}
}, [count])
이 useEffect
는 하나 또는 그 이상의 의존성을 가집니다.
이제 다음 예시를 보며 좀 더 상세하게 Lifecycle 순서를 살펴봅시다.
import React from "react";
function App() {
console.log("App: render start");
const [showChild, setShowChild] = React.useState(() => {
console.log("App: useState(() => false)");
return false;
});
console.log(`App: showChild = ${showChild}`);
React.useEffect(() => {
console.log("App: useEffect(() => {})");
return () => {
console.log("App: useEffect(() => {}) cleanup");
};
});
React.useEffect(() => {
console.log("App: useEffect(() => {}, [])");
return () => {
console.log("App: useEffect(() => {}, []) cleanup");
};
}, []);
React.useEffect(() => {
console.log("App: useEffect(() => {}, [showChild])");
return () => {
console.log("App: useEffect(() => {}, [showChild]) cleanup");
};
}, [showChild]);
const element = (
<>
<label>
<input
type="checkbox"
checked={showChild}
onChange={(e) => setShowChild(e.target.checked)}
/>
show child
</label>
<div>
{showChild ? <Child /> : null}
</div>
</>
);
console.log("App: render end");
return element;
}
import React from "react";
function Child() {
console.log("Child: render start");
const [count, setCount] = React.useState(() => {
console.log("Child: useState(() => 0)");
return 0;
});
console.log(`Child: count = ${count}`);
React.useEffect(() => {
console.log("Child: useEffect(() => {})");
return () => {
console.log("Child: useEffect(() => {}) cleanup");
};
});
React.useEffect(() => {
console.log("Child: useEffect(() => {}, [])");
return () => {
console.log("Child: useEffect(() => {}, []) cleanup");
};
}, []);
React.useEffect(() => {
console.log("Child: useEffect(() => {}, [count])");
return () => {
console.log("Child: useEffect(() => {}, [count]) cleanup");
};
}, [count]);
const element = (
<button onClick={() => setCount((previousCount) => previousCount + 1)}>
{count}
</button>
);
console.log(" Child: render end");
return element;
}
예시를 확인해보고 싶으신 분들은 sandbox 를 클릭하세요.
작성한 코드에 대한 요약입니다.
App
컴포넌트와 Child
컴포넌트를 작성했습니다.App
컴포넌트는 Child
컴포넌트를 보여줄지 말지를 결정하는showChild
state를 가집니다. Child
컴포넌트는 count
state를 가집니다.Child
컴포넌트는 count
값을 변경하는 버튼을 가집니다.useEffect
콜백을 가집니다.다음으로는 아래의 각 단계에서 흐름이 어떻게 보여지는지 살펴보겠습니다 :)
App
이 마운트되는 때App
의 state(showChild)를 변경하여 Child
가 마운트되는 때Child
의 state(count)를 변경하여 Child
가 업데이트되는 때App
의 state(showChild)를 변경하여 Child
가 언마운트되는 때여기서 App
은 마운트 단계에 있으므로 다이어그램에서 순서는 다음과 같아야 합니다.
✅ App의 Lazy initializers 실행
✅ App의 렌더링 실행
✅ 리액트의 App에 대한 DOM 업데이트
❌ App의 Layout Effects Cleanup 실행
✅ App의 Layout Effects 실행
✅ 브라우저의 App에 대한 스크린 그리기
❌ App의 Effects Cleanup 실행
✅ App의 Effects 실행
App
이 마운트될 때, 우리는 다음의 로그들을 확인할 수 있습니다.
App: render start
App: useState(() => false)
App: showChild = false
App: render end
App: useEffect(() => {})
useEffect
가 실행됩니다.App: useEffect(() => {}, [])
useEffect
가 실행됩니다.App
컴포넌트의 마운트 단계이기 때문에 호출되며, 마운트 단계에서는 모든 useEffect 콜백이 호출됩니다.App: useEffect(() => {}, [showChild])
showChild
값에 의존성을 가지는 useEffect
가 실행됩니다.Notes
useEffect
콜백들이 실행됩니다.useEffect
콜백들은 표시되는 순서대로 호출됩니다. (의존성 x -> 빈 의존성 -> 의존성 o)show child
체크박스를 클릭해봅시다.
클릭 시 Child
컴포넌트는 마운트 단계에 오르고, App
컴포넌트는 업데이트 단계를 가질 것입니다.
다이어그램에 따라 Child
의 순서는 다음과 같습니다.
✅ Child의 Lazy initializers 실행
✅ Child의 렌더링 실행
✅ 리액트의 Child에 대한 DOM 업데이트
❌ Child의 Layout Effects Cleanup 실행
✅ Child의 Layout Effects 실행
✅ 브라우저의 Child에 대한 스크린 그리기
❌ Child의 Effects Cleanup 실행
✅ Child의 Effects 실행
그리고 App
의 경우는 다음과 같습니다.
❌ App의 Lazy initializers 실행
✅ App의 렌더링 실행
✅ 리액트의 App에 대한 DOM 업데이트
✅ App의 Layout Effects Cleanup 실행
✅ App의 Layout Effects 실행
✅ 브라우저의 App에 대한 스크린 그리기
✅ App의 Effects Cleanup 실행
✅ App의 Effects 실행
이번엔 찍힌 로그 순서를 확인해봅시다.
App: render start
App: showChild = true
App: render end
Child: render start
Child: useState(() => 0)
Child: count = 0
Child: render end
App: useEffect(() => {}) cleanup
App: useEffect(() => {}, [showChild]) cleanup
showChild
값에 의존성을 가지는 useEffect cleanup이 실행됩니다.showChild
값이 변경되었기 때문에 실행됩니다.Child: useEffect(() => {})
11: Child: useEffect(() => {}, [])
count
값에 의존성을 가지는 useEffect가 실행됩니다.showChild
값에 의존성을 가지는 useEffect가 실행됩니다.showChild
의 값이 변경되었기 때문에 실행됩니다.Notes
App
컴포넌트를 렌더링하는 동안에도 우리는 <Child />
마크업을 가지고 있습니다. 그러나 App
의 렌더링이 종료된 후에야 Child
렌더링이 시작되는 것을 볼 수 있습니다.<Child />
가 Child
함수를 호출하는 것과 다르기 때문입니다. JSX 구문은 기본적으로 React.createElement(Child)
를 호출합니다.Child
를 호출하기 시작할 겁니다.Child
가 보여주는 count
값을 업데이트하기 위해 카운트 버튼을 눌러봅시다.
클릭 시 Child
컴포넌트는 업데이트 단계에 오르고, App
컴포넌트는 변화가 없습니다.
다이어그램에 따라 Child
의 순서는 다음과 같습니다.
❌ Child의 Lazy initializers 실행
✅ Child의 렌더링 실행
✅ 리액트의 Child에 대한 DOM 업데이트
✅ Child의 Layout Effects Cleanup 실행
✅ Child의 Layout Effects 실행
✅ 브라우저의 Child에 대한 스크린 그리기
✅ Child의 Effects Cleanup 실행
✅ Child의 Effects 실행
로그 순서를 확인해봅시다.
Child: render start
Child: count = 1
Child: render end
Child: useEffect(() => {}) cleanup
Child: useEffect(() => {}, [count]) cleanup
count
값에 의존성을 가지는 useEffect cleanup이 실행됩니다.count
값이 변경되었기 때문에 실행됩니다.Child: useEffect(() => {})
Child: useEffect(() => {}, [count])
count
값에 의존성을 가지는 useEffect가 실행됩니다.count
값이 변경되었기 때문에 실행됩니다.거의 다 왔습니다. 조금만 힘내세요😂
Child
컴포넌트를 언마운트시키기 위해 show child
체크박스를 다시 한 번 클릭해봅시다.
클릭 시 Child
컴포넌트는 언마운트 단계에 오르고, App
컴포넌트는 업데이트 단계를 가질 것입니다.
다이어그램에 따라 Child
의 순서는 다음과 같습니다.
❌ Child의 Lazy initializers 실행
❌ Child의 렌더링 실행
❌ 리액트의 Child에 대한 DOM 업데이트
✅ Child의 Layout Effects Cleanup 실행
❌ Child의 Layout Effects 실행
❌ 브라우저의 Child에 대한 스크린 그리기
✅ Child의 Effects Cleanup 실행
❌ Child의 Effects 실행
그리고 App
의 경우는 다음과 같습니다.
❌ App의 Lazy initializers 실행
✅ App의 렌더링 실행
✅ 리액트의 App에 대한 DOM 업데이트
✅ App의 Layout Effects Cleanup 실행
✅ App의 Layout Effects 실행
✅ 브라우저의 App에 대한 스크린 그리기
✅ App의 Effects Cleanup 실행
✅ App의 Effects 실행
찍힌 로그 순서를 확인해봅시다.
App: render start
App: showChild = false
App: render end
Child: useEffect(() => {}) cleanup
Child: useEffect(() => {}, []) cleanup
Child: useEffect(() => {}, [count]) cleanup
count
값에 의존성을 가지는 useEffect cleanup이 실행됩니다.App: useEffect(() => {}) cleanup
App: useEffect(() => {}, [showChild]) cleanup
showChild
값에 의존성을 가지는 useEffect cleanup이 실행됩니다.showChild
값이 변경되었기 때문에 실행됩니다.App: useEffect(() => {})
App: useEffect(() => {}, [showChild]);
showChild
값에 의존성을 가지는 useEffect가 실행됩니다.showChild
의 값이 변경되었기 때문에 실행됩니다.그리고 마지막으로, App
컴포넌트가 언마운트될 때, App
의 모든 useEffect cleanup
들이 실행되면서 라이프사이클이 종료됩니다.
useEffect의 호출 순서와 시기는 개발할 때마다 매번 조금씩 헷갈리는 내용인 것 같습니다. 부모와 자식 중 누구의 useEffect가 먼저 실행되는지, 컴포넌트 내 여러 개의 useEffect 중 어떤 것이 제일 먼저 호출되는지 등 말이죠.
이번 글을 번역하면서 부모와 자식 컴포넌트에서 어떤 순서로 렌더링과 useEffect cleanup, useEffect 콜백이 실행되는지 제대로 이해할 수 있었던 시간이었습니다.
로그를 살펴보는 부분에서 비슷한 문구가 반복되다 보니 다소 가독성이 떨어집니다. useEffect cleanup이 실행되는 것과 useEffect가 실행되는 부분을 잘 구분하셔서 리액트 훅의 라이프사이클을 이해하는 데 도움이 되시길 바라겠습니다 :)