리액트 프레임워크는 컴포넌트를 생성하고 렌더링하다가 어떤 시점이 되면 소멸합니다. 이러한 과정을 컴포넌트의 생명 주기 라고합니다. 컴포넌트의 생명 주기는 클래스 컴포넌트가 더 직관적이므로 클래스 컴포넌트로 시계를 만들면서 리액트 컴포넌트의 생명 주기에 관해 알아보겠습니다.
클래스 컴포넌트에서 상태 구현하기
클래스 컴포넌트에서 상태는 항상 state라는 이름의 멤버 속성으로 구현해야 한다는 제약 조건이 있습니다. 다음 코드에서는 state 속성에 다시 today멤버 속성을 만들고 초깃값을 설정해 제약 조건을 만족게 합니다. 그리고 render() 메서드에서 this.state.today 형태로 today 멤버 속성값을 얻을수 있는데 객체에 적용하는 비구조화 할당 구문을 사용해 의미를 좀 더 분명하게 했습니다.
클래스 컴포넌트에서 상태 구현 패턴 export default class ClassLifecyle extends Component { state = { today: new Date } render() { const {today} = this.state ...(생략)... } }
컴포넌트 마운트
리액트 컴포넌트는 가상 Dom 객체 형태로 생성되어 어떤 시점에 물리 DOM 트리의 멤버 객체가 되며, 이과정에서 처음 렌더링이 일어나는데 이 시점을 컴포넌트가 마운트 되었다고 표현합니다. 즉 , 가상 DOM 객체가 물리 DOM 객체로 바뀌는 시점을 마운트되었다고 표현합니다.
리액트는 클래스 컴포넌트가 componentDidMount()라는 메서드를 가지면 마운트되는 시점에 이 메서드를 호출해 줍니다. 다음 코드에서는 컴포넌트가 마운트되는 시점에 시계를 만들고자 componentDidMount() 메서드에서 setInterval() 함수를 호출했습니다.
컴포넌트가 마운트될 때 시계 만들기 export default class ClassLifecyle extends Component { componentDidMount() { const duration = 1000 const intervalId = setInterval(콜백_함수, duration) } }
이제 콜백 함수를 구현해야 하는데 이때 모든 클래스 컴포넌트의 부모 클래스인 Component 클래스가 제공하는 setState() 메서드가 필요합니다.
setState( ) 메서드
Component클래스는 다음과 같은 setState() 메서드를 제공합니다.
setState() 정의 setState<K extends keyof S>( state: ((prevState: Readonly<S>, props: Readonly<P>) => (Pick<S, K> | S | null)) | (Pick<S, K> | S | null), callback?: () => void): void;
setState() 메서드는 타입 정의가 매우 복잡해 보이지만 대부분 다음처럼 호출합니다.
setState() 호출 예 const id = setInterval(콜백_함수, 시간_간격) this.setState({intervalId: id})
setState()는 클래스가 state라는 이름의 멤버 속성을 가지고 있다는 가정으로 성계된 메서드 입니다.
앞서 클래스 컴포넌트가 상태를 가지려면 반드시 state라는 멤버 속성이 있어야 한다고 한 이유는 setState()메서드가 이런 방식으로 구현되었기 때문입니다.
1초마다 현재 시각 갱신하기 export default class ClassLifecycle extends Component { state = { today: new Date(), intervalId: null as unknown as NodeJS.Timer // 타입스크립트가 요구하는 구현 방식 } componentDidMount() { const duration = 1000 const intervalId = setInterval(() => this.setState({today: new Date()}), duration) this.setState({intervalId}) } }
클래스 컴포넌트의 언마운트
컴포넌트가 생성되고 마운트되어 웹 페이지에 나타난 후 어떤 시점이 되면 컴포넌트는 소멸 합니다. 리액트에서는 컴포넌트가 물리 DOM 객체로 있다가 소멸하는 것을 언마운트 되었다고 표현합니다. 그리고 리액트는 클래스 컴포넌트가
componentWillUnmount() 메서드를 구현하고 있으면 언마운트가 일어나기 직전에 이 메서드를 호출해 줍니다.
앞선 코드에서는 componentDidMount() 메서드에서 setInterval() 함수를 호출했는데, setInterval()은 더 이상 사용하지 않으면 반드시 clearInterval() 함수가 호출될거라는 전제로 설계되었습니다 만일 clearInterval()을 호출하지 않으면 메모리 누수가 발생합니다.
언마운트 직전에 메모리 해제 componentWillUnmount() { clearInterval(this.state?.intervalId) }
두 훅은 사용법이 같으며 콜백 함수는 훅이 실행될 때 처음 한번은 반드시 실행됩니다. 이런 특징 때문에 의존성 목록이 빈 배열[]일지라도 한 번은 콜백 함수를 호출합니다.
그리고 이런 설계는 클래스 컴포넌트의 componentDidMount() 메서드를 구현한 것과 사실상 같은 효과를 보입니다. 두 훅이 componentDidMount()와 다름 점은 단 한 번 실행되는 componentDidMount()와 달리, 두 훅은 의존성 목록이 변경될 때마다 콜백 함수를 계속 실행 한다는 점입니다.
두 훅의 콜백 함수는 다음처럼 함수를 반환할 수 도 있는데, 이때 반환 함수는 컴포넌트가 언마운될 때 한번만 호출 됩니다.
한 번만 호출되는 반환 함수 콜백_함수() => { return 반환_함수 }
useLayoutEffect와 useEffect 훅의 차이점
리액트 프레임워크는 useLayoutEffect 훅은 동기로 실행하고, useEffect훅은 비동기로 실행합니다. 이 말은 useLayoutEffect 훅은 콜백 함수가 끝날 때까지 프레임워크가 기다린다는 의미입니다. 반면에 useEffect는 콜백 함수의 종료를 기다리지 않습니다. 리액트 공식문서를 보면 될 수 있으면 useEffect훅을 사용하고 useEffect가 구현이 안될때만 useLayoutEffect 훅을 사용하라고 권합니다.
리액트 개발을 하다 보면 가끔 DOM이나 window 객체에 이벤트 처리기를 부착해야 할 때가 있습니다. HTMLElement와 같은 DOM 타입들은 EventTarget 타입의 상속 타입으로, EventTarget은 addEventListener()와 removeEventListener()라는 메서드를 제공합니다.
그런데 앞서 setInterval처럼 addEventListener() 메서드를 호출하면서 반드시 removeEventListener() 메서드를 호출해 주어야 메모리 누수가 발생하지 않습니다. 이러한 내용을 useEventListener라는 커스텀 훅으로 만들어 보겠습니다.