React _ LifeCycle & useEffect

kyle kwon·2022년 10월 19일
0

React

목록 보기
2/15
post-thumbnail

Prologue

블로그를 이전하면서 이전 블로그의 내용을 옮겼습니다.

리액트를 공부하면서 지난 시간 State hook에 이어서, 공식문서를 바탕으로 LifeCycle과 Effect hook을 배우면서, 얻은 지식과 내용들을 남겨보려 합니다. 지난 시간 State hook을 공부하면서, 약간의 혼돈이 온 부분이 있었는데요. 바로 기본적인 코드에서 혼돈이 왔었습니다.

아래의 코드를 봅시다.

import React, { useState } from "react"

export default function App() {
	const [number, setNumber] = useState(1)
	const add = () => setNumber((number) => number + 1)
    
    // jsx
    return(
    	<div>
            <h1> Number : {number} </h1>
            <div>
                <button onClick={add}> + 1</button>
            </div>
        </div>
    )
}

코드에서 왜 setNumber(setState함수)에 왜 콜백 함수를 넣어줘야 하는지 순간 이해가 되지 않았었습니다.

지난 시간에 react가 렌더링 되는 순간은 1) props가 바뀔 때, 2) state가 변경될 때, 3) 자식 컴포넌트를 포함하고 있는 부모 컴포넌트가 리렌더링 될 때 다시 렌더링이 발생한다고 했습니다. 


왜 콜백 함수를 넣어줘야 하는지, 그 이유를 다음과 같이 설명할 수 있습니다.

setState 함수는 비동기 함수이기 때문에 인자로 콜백 함수를 넣을 수 있습니다. 참고로, 클래스 컴포넌트로 만들 때와 달리, 인자를 한 개만 받습니다. 이전 상태값을 콜백함수 내에 인자로 받아서, 이 인자를 변경할 콜백 함수 내 로직을 거쳐, 변경된 값을 반환하여야 변경된 값을 렌더링 할 수 있기 때문입니다.



이 useState hook의 내부 상태변경 함수 setState의 비동기와 연결되어, 앞으로 다룰 useEffect hook에 대한 이야기가 나오는데요. 클래스 컴포넌트의 경우 setState를 다룰 때 인자 2개를 받는데, useState hook을 사용하는 함수형 컴포넌트에서는 인자를 1개만 받습니다. 대신, useEffect hook에서는 인자를 2개로 다루게 됩니다. 

여기서도 약간 혼돈이 오기 시작했었습니다🤔
그럼, useEffect와 빼놓을 수 없는 LifeCycle를 함께 다뤄 얘기해보겠습니다.



What is LifeCycle in React?

LifeCycle, 즉 '생명주기'란 리액트에서 컴포넌트가 mount(생성) 되었다가, unmount(제거)될 때까지의 주기(기간)이라고 말할 수 있습니다.

https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

💡 참고로 위에서 말하는 React DOM은 Virtual DOM이라고 할 수 있습니다.


◾️ 공식문서를 바탕으로 LifeCycle이 어떻게 실행되는지 아래의 리액트 코드를 이용해서, 절차 단계를 분리하여 기술해 보겠습니다.

import React from "react"

export default class Clock extends React.Component{
	constructor(props) {
		super(props); 
		this.state = {date : new Date()}
	}
    
 	tick() {
    	this.setState({
      	date: new Date(),
    	})
  }
    
	componentDidMount() {
    	console.log("componentDidMount")
    	this.timerID = setInterval(() => this.tick(), 1000)
  }
  
	componentWillUnmount(){
    	console.log("componentWillUnmount");
        clearInterval(this.timerID);
    }
    
	render() {
		return (
			<div>
				<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
			</div>
		)
	}
}

생명주기 구분 단계

c.f componentDidMount와 componentDidUpdate, componentWillUnmount 메서드는 모두 생명주기 메서드입니다.

생성될 때

  1. constructor를 실행하여, 값을 초기화 한다.
  2. state에 date 객체의 키에 접근해 new Date()로 변경시켜준다.
  3. render()가 실행된다.
  4. React DOM이 업데이트 된다.
  5. React Dom이 업데이트 된 직후, componentDidMount() 메서드가 실행되는데, 이는 component가 Mount 되었다는 것을 의미한다.

업데이트 할 때

  1. setState() 실행에 의해, state가 업데이트 된다.
  2. 다시, render()가 실행된다.
  3. React DOM이 다시 업데이트 된다.
  4. 업데이트 된 후 componentDidUpdate() 메서드가 실행되는데, 위의 코드에서는 내부적으로 따로 실행할 내용이 없다.

제거할 때

  1. componentWillUnmount 메서드를 실행한다.

보통, setInterval()과 같은 메서드를 componentDidMount 메서드에서 사용한다면, clearInterval()과 같은 메서드를 WillUnmount 시에 사용한다. 즉, 묶음으로 사용하는 경우가 많다.

이 생명주기와 연관지어서, 이제 Effect Hook에 대해서 이야기 할 수 있습니다.




useEffect

리액트 공식문서에서는 다음과 같이 이야기하고 있습니다.

React 컴포넌트 안에서 데이터를 가져오거나, DOM을 직접 조작하는 작업을 이전에도 종종 해봤을 것입니다. 우리는 이런 동작을 side Effects 또는 effects 라고 합니다. 왜냐하면, 이것은 다른 컴포넌트에 영향을 줄 수도 있고, 렌더링 과정에서는 구현할 수 없는 작업이기 때문입니다.useEffect Hook을 이용하여 우리는 React에게 컴포넌트가 렌더링 이후에 어떤 일을 수행해야하는 지를 말합니다.

위의 내용을 통해, Effect Hook이 나온 배경을 알 수 있게 되었습니다. 즉, useEffect는 함수 컴포넌트 내에서 이런 저런 side Effects를 수행할 수 있게 도와준다는 것을 알 수 있습니다. side Effects는 영단어를 직역해보면, 부작용이라고 이야기 할 수도 있지만, 리액트에서 상태관리는 필수적이고, 이 상태가 변경되는 현상을 side Effects라고 역설적으로 바라보면 될 것 같습니다.


그리고, 앞에서 봤던 생명주기 메서드 3가지와 같은 목적으로 작동하지만, 하나의 API로 통합된 것이라고 볼 수 있습니다.

◾️ useEffect의 구조는 아래의 2가지 경우와 같습니다.

// 경우 1

export default function App(){
	useEffect(() => {
    
    }, [])
    
    return (
    	<div>App is loading...</div>
    )
}

// 경우 2

export default function App(){
	useEffect(() => {
    
    })
    
    return (
    	<div>App is loading...</div>
    )
}

경우 1빈 배열을 두 번째 인자로 활용하는 경우입니다.
이는 생명주기 메서드에서 componentDidMount처럼 동작하는 데요. return 부분에서 jsx 요소들이 첫 렌더링이 일어난 직 후, mount된 후에 내부의 화살표 함수를 가져와 실행합니다.


경우 2는 빈배열을 두 번째 인자로 받지 않고, 콜백 함수만 받는 경우 입니다.
이는 생명주기 메서드에서 componentDidMount + componentDidUpdate처럼 동작하는데요.
첫 번째 렌더링 된 직후에, 함수가 실행되고, props를 받거나 state가 업데이트 되면, 다시 리렌더링이 발생합니다. 


일반적으로는 경우 2처럼 잘 사용되지 않는데요. 아래의 코드와 같이 사용됩니다.

export default function App(props){
	const [state, setState] = useState(initValue);
    
    useEffect(() => {
    
    }, [state, props.a])
    
    return(
    	<div> App is loading...</div>
    )
}

경우 1처럼 배열을 받지만, 빈배열이 아닌 state와 props가 담겨 있습니다.
이 둘을 객체로서 props와 state가 변경되면 리렌더링이 발생하기 때문에, 위 코드에서 useEffectcomponentDidMount + 특정 값이 변경될 때만 실행되는 componentDidUpdate처럼 동작하게 됩니다.

이처럼 두 번째 인자로 들어오는 배열을 의존성 배열(Array Dependencies)라고 칭합니다. state의 상태 변경 또는 props의 변경에 의존한다는 이야기이겠죠?


💡 useEffect를 위와 같이 사용하면 좋은 점은 어떤 것이 있을까요?

state가 setState함수에 의해 업데이트 된 뒤에 렌더링을 하면 이전에 렌더링 된 값과 얕은 비교를 하는데요. 비교했을 때 다른 경우, React가 useEffect를 실행하고, 같으면 건너뛰게 합니다. 이는 componentDidUpdate를 건너뛰게 해서 최적화를 가능케 한다는 장점이 있습니다.



◾️ 그렇다면, useEffect를 생명주기 메서드 중 componentWillUnmount처럼 사용하려면 어떻게 해야할까요?

다음과 같습니다.

export default function App(props){
	const [state, setState] = useState(initialState);
    
    useEffect(() => {
    	return () => {
        	cleanup
        }
    }, [state, props.a])
    
    return (
    	<div>App</div>
    )
}


그럼 위에 LifeCycle을 설명하면서 들었던 예시를, useEffect 함수를 이용해서 다음과 같이 만들 수 있습니다. 단, Hook 사용을 위해서는 클래스 컴포넌트에서 함수형 컴포넌트로 변환시켜줘야 합니다.

import React, { useState, useEffect } from "react"

function Clock() {

	const [date, setDate] = useState(new Date());
    
    const tick = () => {
    	setDate(new Date());
    }
    
    
    useEffect(() => {
    	console.log('componentDidMount');
        const timerId = setInterval(tick,1000);
        
        return () => {
        	console.log('componentWillUnmount');
            clearInterval(timerId);
        }
    }, [])
    
    
    useEffect(() =>{
    	console.log('componentDidUpdate');
        console.log(date);
    }, [date])
    
    return (
    	<div>
        	<p> 현재 시각은 | {date.toLocaleTimeString()}</p>
        </div>
    )
 }
 
 export default Clock
  1. useEffect 내부에 첫 번째 인자로 함수를 받고, 두 번째 인자로 빈배열을 넣으면, componentDidMount처럼 동작한다는 것을 알고 있습니다. 따라서, useEffect 내부 첫 번째 인자의 함수는 setInterval을 실행해서, 1초마다 date를 업데이트 시켜주는 tick함수를 호출합니다. 즉, mount 될 때 tick 함수를 호출한다는 의미입니다.
  1. componentDidUpdate처럼 동작하기 위해서는, state 값의 변경 또는 props가 들어오는 것을 확인할 수 있도록, useEffect를 분리해서 새로 작성하고, 두 번째 인자로 state를 담은 배열을 넣어주면, update 될 때마다, 첫 번째 인자인 콜백 함수를 실행합니다.
  1. componentWillUnmount처럼 동작하기 위해서는, useEffect의 첫 번째 인자인 콜백함수가 함수를 return 하도록 하면 되는데, 위의 코드에서는 clearInterval이라는 메서드를 담은 함수를 return 하여 unmount될 때 실행하고 있다.

componentDidUpdate처럼 동작하는 부분만, 따로 분리해서 작성했는데, 이것이 바로 위에 setInterval과 clearInterval이 같은 useEffect 안에 있어야 가독성이 좋아진다는 장점만 있는 것이 아니라, 성능 최적화 측면에서도 유리하게 하는 것입니다.

즉, update가 발생하는 경우에만, effect를 실행하도록 코드를 분리하기 위함입니다.




Conclusion

이렇게 리액트의 근간이 되는 hook 중에 useState Hook을 회고하고, useEffect hook에 대해서 알아보았습니다. 저 개인적으로도 이렇게 글로 작성하면서, 한 번 더 머릿 속에서 정리할 수 있는 좋은 시간이 되었던 것 같습니다. 이 두 hook을 이용해서, 작은 기능 구현을 해보려 합니다. 다음 글은 구현해 보면서, 어떤 점이 구현하기 어려웠고, 어떻게 hook을 활용했는지 회고해 보겠습니다.

profile
FrontEnd Developer - 현재 블로그를 kyledot.netlify.app으로 이전하였습니다.

0개의 댓글