컴포넌트의 라이프사이클 & Hooks (1)
🤔 컴포넌트의 라이프사이클 이란?
왜 위 주제를 알아보고 싶었는가?
React 를 학습하면서 Hooks 의 종류별 사용 방법을 학습 중에 있는데 정확한 이해를 위해서는 컴포넌트의 라이프사이클에 대한 학습이 필수적이라 생각했기 때문이다.
학습에 주로 참고한 자료는 리액트를 다루는 기술 책을 참고하였고, 추가적으로 자료를 찾아보면서 적었습니다.
컴포넌트의 라이프사이클이란?
React 에서 사용하는 모든 컴포넌트에는 라이프사이클이 존재한다. 컴포넌트의 수명은 페이지에 렌더링되기 전인 준비과정에서 시작하여 페이지에서 사라질 때 끝이난다.
프로젝트나 학습 중 컴포넌트가 렌더링 할 때, 업데이트 전 후로 어떠한 작업을 진행하는 경우가 있을 것이다.
이럴 때 사용하는 것이 클래스형 컴포넌트에서는 라이프사이클 메서드, 함수 컴포넌트에서는 Hooks (완전하게 동일하게 동작하는 것은 아니고 약간의 차이는 있다.) 이다.
라이프사이클 메서드를 토대로 컴포넌트의 라이프사이클을 알아보려고 한다. 라이프사이클 메서드의 종류는총 아홉 가지가 있고, 이 메서드들을 활용한 동작이 크게 마운트, 업데이트, 언마운트 가 존재한다.
아래부터 알아볼 내용은 마운트, 업데이트, 언마운트가 어떠한 라이프사이클 메서드를 통해서 동작하며,각 메서드는 어떠한 동작을 하는지 알아볼 예정이다.
들어가기 전에 하나 알아가면 좋은 것은 어떠한 동작을 하기 전에 발동되는 것은 Will, 동작을 하고 난 이후에 발동되는 것은 Did 라는 접두사가 붙는다는 것을 알고 들어가면 좋다.
또한 이는 클래스형 컴포넌트에서 사용되는 라이프사이클 메서드를 알아보는 것이기 때문에 함수 컴포넌트를 기준으로 생각하면 맞지 않을 수 있다.
👨🏻🏫 라이프사이클 메서드 종류
마운트
DOM 이 생성되고 웹 브라우저상에 나타나는 것을 마운트(mount) 라고 한다.
이 과정에서 호출하는 메서드는 순서대로 construct → getDerivedStateFromProps → render → componentDidMount 이다. 하나씩 어떠한 함수인지 알아보면 다음과 같다.
-
construct: 컴포넌트를 새로 만들 때마다 호출되는 클래스 생성자 메서드이다.
- 컴포넌트의 생성자 메서드로 컴포넌트를 만들 때 처음으로 실행된다.
- 초기 state 를 정할 수 있다.
-
getDerivedStateFromProps: props 에 있는 값을 state 에 넣을 때 사용하는 메서드이다.
- React v16.3 이후에 새로 만든 라이프사이클 메서드로, props 로 받아 온 값을 state 에 동기화하는 용도로 사용된다.
- 컴포넌트가 마운트될 때, 업데이트될 때 호출이 된다.
- 리턴 값으로 state 변경 필요 유무를 보내주는데 이를 간단한 코드로 보면 아래와 같다.
static getDerivedStateFromProps (nextProps, prevState) { if(nextProps.value !== prevState.value) return { value: nextProps.value }; return null; }
-
render: 준비해둔 UI 를 렌더링하는 메서드 이다.
- 컴포넌트의 모양새를 정의하기 때문에 라이프사이클 메서드 중 유일한 필수 메서드이다.
- 이 메서드 안에서 this.props, this.state 에 접근할 수 있으며, 리액트 요소를 반환한다. 반환하는 것은 단순 HTML 태그일 수 있고, 따로 선언한 컴포넌트 일 수 있다.
- 주의할 점은 이 메서드 안에서는 이벤트 설정이 아닌 이상 setState 를 사용하면 안되며 브라우저의 DOM 에 접근해서도 안된다.
-
componentDidMount: 컴포넌트가 웹 브라우저상에 나타난 후 호출하는 메서드이다.
- Did 가 붙은 것으로 알 수 있듯 이 메서드는 컴포넌트를 만들고 첫 렌더링을 다 마친 후 실행된다.
- 이 메서드 내부에서 다른 자바스크립트 라이브러리 또는 프레임워크의 함수를 호출하거나, 이벤트 등록, setTimeout, setInterval, 네트워크 요청 같은 비동기 작업을 처리하면 된다.
업데이트
말 그대로 컴포넌트가 업데이트 될 때를 말하며 주로 4가지 경우에 업데이트가 이루어진다.
- props 가 바뀔 때
- state 가 바뀔 때
- 부모 컴포넌트가 리렌더링될 때
- this.forceUpdate 로 강제로 렌더링을 트리거할 때
업데이트가 이루어질 때 호출되는 라이프사이클 메서드의 순서는 아래와 같다.
업데이트 발생 → getDerivedStateFromProps → shouldComponentUpdate → [true 인 경우만 이후 메서드 진행] → render → getSnapShotBeforeUpdate → componentDidUpdate
중복된 내용을 제외하고 하나씩 어떠한 함수인지 알아보면 다음과 같다.
-
shouldComponentUpdate: props 또는 state 를 변경했을 때, 리렌더링을 시작할지 여부를 리턴하는 메서드이다.
- 반드시 true, false 값을 반환해야 한다.
- 컴포넌트를 만들 때 위 메서드를 설정하지 않으면 기본적으로 언제나 true 가 반환된다.
- 특정 함수에서 this.forceUpdate() 함수를 호출한다면 이 과정을 생략하고 render 함수를 호출한다.
- 위 메서드 안에서 현 props 와 state 는 this 를 붙여서 접근하고, 새로 설정될 props 와 state 는 nextProps, nextState 로 접근할 수 있다.
- 이 메서드를 통해서 결국 리렌더링을 컨트롤하기 때문에 성능을 최적화 할 때 적절하게 리렌더링 방지를 위해서 false 를 반환하게 하면 좋다.
-
getSnapshotBeforeUpdate: render 에서 만들어진 결과물이 브라우저에 실제로 반영되기 직전에 호출되는 메서드
- React v16.3 이후 만들어진 메서드이다.
- 반환하는 값이 componentDidUpdate 의 세 번째 파라미터인 snapshot 값으로 사용된다.
- 업데이트하기 직전의 값을 참고할 일이 있을 때 사용할 수 있다.
- 아래는 스크롤바 위치 유지를 위한 예제 코드이다.
getSnapshotBeforeUpdate(prevProps, prevState) { if(prevState.array !== this.state.array) { const { scrollTop, scrollHeight } = this.list; return { scrollTop, scrollHeight }; } }
-
componentDidUpdate: 리렌더링이 완료된 후 실행이 된다.
- 업데이트가 다 완료된 이후에 실행되는 함수이기 때문에 DOM 관련 처리를 해도 무방하다.
- prevProps 혹은 prevState 를 사용하여 컴포넌트가 이전에 가졌던 데이터에 접근할 수 있다.
- getSnapshotBeforeUpdate 에서 반환한 값이 있다면 여기서 snapshot 값을 전달받을 수 있다.
componentDidUpdate(prevProps, prevState, snapshot) { ... }
언마운트
마운트의 반대 과정으로 컴포넌트를 DOM 에서 제거하는 것을 언마운트(unmount) 라고 한다.
언마운트시에는 componentWillUnmount 메서드만 실행이 된다.
- componentWillUnmount: 컴포넌트가 웹 브라우저상에서 사라지기 전에 호출하는 메서드이다.
- 컴포넌트를 DOM 에서 제거할 때 실행된다.
- componentDidMount 에서 등록한 이벤트, 타이머, 직접 생성한 DOM 이 있다면 여기서 제거를 해야한다.
눈치가 빠른 사람이면 9가지의 메서드 중 하나가 비어있다는 것을 알 수 있다.
남은 하나는 componentDidCatch 라는 메서드이다.
-
componentDidCatch: 컴포넌트 렌더링 도중에 에러가 발생했을 때 오류 UI 를 보여 줄 수 있는 메서드이다.
componentDidCatch(error, info) { this.setState({ error: true, }); // 에러 발생시 원하는 코드 기입을 하면 된다. console.log({ error, info }); }
-
여기서 error 는 파라미터에 어떤 에러가 발생했는지 알려주며, info 파라미터는 어디에 있는 코드에서 오류가 발생했는지에 대한 정보를 준다.
-
이 메서드를 사용할 때 컴포넌트 자신에게 발생하는 에러를 잡아낼 수 없고 자신의 this.props.children 으로 전달되는 컴포넌트에서 발생하는 에러만 잡아낼 수 있다.
-
아래 코드는 위 함수를 클래스형 컴포넌트에서 사용하는 예제이다.
import { Component } from 'react'; class ErrorBoundary extends Component { state = { error: false, }; componentDidCatch(error, info) { this.setState({ error: true, }); console.log({ error, info }); } render() { if(this.state.error) return <div>에러가 발생했습니다!</div>; return this.props.children; } } export default ErrorBoundary; // app.js render() { ... <ErrorBoundary> // 컴포넌트들 </ErrorBoundary> ... }
-
정리
라이프사이클 메서드는 컴포넌트 상태에 변화가 있을 때 마다 실행되는 메서드이다. 이 메서드들은 서드파티
라이브러리를 사용하거나 DOM 을 직접 건드려야 할 상황에 유용하다. 특히 컴포넌트의 업데이트 성능을
개선할 때 shouldComponentUpdate 메서드가 유용하게 사용된다는 점을 기억해야 한다.
🤯 Hooks 란?
Hooks 란 React v16.8 에 새롭게 도입된 기능으로 함수 컴포넌트에서도 상태 관리를 할 수 있는 useState, 랜더링 직후 작업을 설정하는 useEffect 등의 기능을 제공하여 기존의 함수 컴포넌트에서 할 수 없었던 작업을 할 수 있게 해주는 기능이다.
이 기능이 어떻게 만들어진 것인지 생각을 해본다. 함수에 어떠한 값을 저장하고 유지하는 기능을 우리는 이전에 본 적이 있을 것이다. 바로 클로저이다. React 에서 제공하는 가장 간단한 Hook 인 useState 를 클로저를 활용해서 만들어보면 아래와 같다.
const React = (function () {
let _states = [];
let idx = 0;
function useState(initialValue) {
const state = _states[idx] || initialValue;
const currIdx = idx;
function setState(value) {
_states[currIdx] = value;
}
idx += 1;
return [state, setState];
}
function render(Component) {
return Component();
}
return { useState, render };
})();
렌더링 관련 부분을 제거한 아주 단순한 구조이지만 useState 를 만들었다.
내부적으로 state 들을 저장하기 위한 배열이 존재하고, 인덱스를 통해 배열 내부의 state 에 접근할 수 있도록 하는 클로저를 활용한 구조이다.
리액트가 아주 멋있는 기능을 제공하는 듯 보이지만 (내부를 열어보고 다양한 Hooks 를 보면 실제로는 엄청 멋있는 것이 맞다.) 자바스크립트 기본 요소들을 활용해서 만든다는 것을 알 수 있다.
다음 글에서는 Hooks 에 어떠한 Hook 이 존재하며 어떻게 사용하는지 알아보도록 하겠다.
🧐 짧은 소감
Hooks 나 컴포넌트를 다루면서 편한 부분만 느끼고 이게 왜 동작하는지에 대한 이해가 부족했는데 나름은 어떠한 구조로 진행되는지를 알게 된 것 같아서 만족한다. 하지만 내부를 깊게 살펴보지는 못한게 아쉬워 다음에는 더 깊게 살펴보는 시간을 가져볼까 한다.
또한 클래스형 컴포넌트와 함수 컴포넌트의 동작에는 차이가 있다는 것을 알게되었는데 Hooks 를 살펴보며 이에 대해서도 더 자세하게 알아보려 한다.