React

리액트 라이프사이클과 useEffect 실행순서

hihiha2 2023. 9. 6. 17:26

useEffect를 이해하기 위해서는 리액트라이프 사이클에 대한 이해가 선행되어야한다.

➡️ useEffect를 정확하게 이해하기 위해서, 리액트 라이프사이클을 공부했다.

 

useEffect를 공부하면서 다른 부분보다 제일 헷갈렸던 부분이 실행순서였다.

훅스의 라이프사이클뿐만 아니라 클래스형의 라이프사이클를 알면 이 부분이 조금 더 쉽게 이해가 된다.

이 글은 리액트 라이프사이클이란 무엇인가, useEffect는 어떤 실행순서로 진행되는가를 중점적으로 다룰 것이다.

(useEffect를 이해하기 위해 공부한 것이기 때문에, 글은 클래스형보다는 useEffect훅에 더 초점이 맞춰질 것이다.)

 

 

 

🖥 리액트 라이프사이클이란?

"생명주기 메서드"

컴포넌트가 생성, 업데이트, 소멸되는 과정을 단계적으로 관리하고 제어하는 방법

컴포넌트가 브라우저에서 초기 렌더링, 재렌더링, 제거 이렇게 각 시점에서 실행하고 싶은 행위를 설정하기 위해서 이용한다.

➡️ 쉽게 말해서 브라우저 화면에서 보여지는 것을 기준으로, 특정작업을 실행할 시점을 결정하는 것이라고 이해하면 편하다.

예를 들면, 프로젝트를 만들면서 API를 호출하여 데이터를 받아와야 하는 상황이 있다고 해보자.

호출된 API를 이용하여 state의 값을 변경하면 화면이 재렌더링이 된다. 이렇게 재렌더링이 되면 코드를 다시 읽어들이고 API를 호출하는 함수는 또 호출된다. 그러면 다시 재렌더링이 일어나고.. 호출하고.. 재렌더링.. 호출.. 무한반복될것이다.

 

API를 통해서 서버에서 영화 데이터를 받아와서 화면에 띄워줘야하는 상황인데, 만약 라이프사이클 관리 없이 API를 호출한다면 위와 같이 계속해서 데이터를 호출하고 그에 따른 재렌더링이 계속해서 발생하는 문제가 생긴다.

 

이럴때 우리는 언제 API를 호출할지 시점을 결정하는 것이 필요하다.

➡️ "처음 화면에 렌더링될때, 1번만" 

 

 

 

 

🖥 기존의 클래스형 라이프사이클관리

 

 

함수형이전에 많이 사용되던 클래스형 라이프사이클 관리의 경우, 여러가지의 메서드를 사용한다.

(현재는 함수형을 대부분 사용하기 때문에 componentDidMount, componentDidUpdate, ComponentWillUnmount를 중점으로 읽어보면 될 것 같다.)

 


각각의 기능을 보면 아래와 같다.

 

 

constructor: 컴포넌트가 가지고 있는 state등의 초기설정, 컴포넌트가 만들어지는 과정에서 미리 해야할 일을 처리

처음 만들어질때 

 

getDerivedStateFromProps: props로 받은 값을 state로 동결시키고 싶다

(마운팅될때 / state업데이트될때 모두 실행)

 

render: 어떤 돔을 만들게 될지 , 내부의 태그들에 어떤 값을 전달할지 전달

 

componentDidMount: 브라우저상에 최초로 실제로 나타날때

컴포넌트가브라우저에 나타난 시점에 어떤 작업을 하겠다!! 라는 것을 명시해주는것

주로 이벤트를 리스닝한다던지 api요청등을 한다.

(이 함수안에서 특정돔에 무언가를 그려달라고 하거나 api, ajax요청을 처리, 컴포넌트가 나타나고 몇초뒤에 뭔가를 하고 싶다, 컴포넌트가 나타나고 해당돔에서 스크롤이벤트를 하겠다)

 

shouldComponentUpdate: 컴포넌트가 업데이트되는 성능을 최적화하고 싶을때 사용 / 버추얼 돔에 렌더링할지 말지를 정함

컴포넌트는 부모컴포넌트가 리렌더링되면 자식컴포넌트가 다 렌더함수가 실행되게 되어있다.

렌더함수가 실행되면 자식 컴포넌트가 버추얼돔에 렌더링을 한다. (실제돔이 아님)

그런데 이때 그 버추얼돔에 렌더링하는 것조차 성능을 아끼고 싶다!! 하면 shouldComponentUpdate를 사용한다.

만약 컴포넌트가 몇백개 몇천개라면, 버추얼 돔에 렌더하는것도 아껴야한다.

true, false값을 가지고 false를 반환하면 브라우저 화면에 나오지 않는다.

 

getSnapshotBeforeUpdate: 버추얼돔에 렌더링을 한 뒤, 브라우저 렌더링 바로 직전에 호출되는 함수

스크롤의 위치, 해당 돔의 크기등을 가져오고 싶다! 하면 사용

 

componentDidUpdate: 이전의 상태와 지금의 상태가 다르면, 이 안에 있는 작업을 하겠다

 

componentWillUnmount: componentDidMount에서 설정한 이벤트 리스너를 없앤다.


🔫 useEffect 실행순서

useEffect는 위의 componentDidMount()  + ComponentDidUpdate() + componentWillUnMount() 의 기능을 합친 것.

➡️ 기본적으로 useEffect는 마운트 이후에 이루어지고, state변화로 재렌더링 이후, 언마운트 이전에 이루어진다.

 

이런 실행순서는 기존의 클래스형에서 라이프사이클을 관리하는 것을 공부하면 더 잘 이해된다.

 

🎨 componentDidMount() 

마운트 (Mounting): 컴포넌트가 생성되고 DOM에 추가되는 단계

  • constructor: 컴포넌트 객체가 생성
  • static getDerivedStateFromProps: props로부터 상태를 설정
  • render: 컴포넌트의 UI를 렌더링
  • componentDidMount: 컴포넌트가 DOM에 추가된 후 호출됩

componentDidMount는 영어 그대로 직역해서 생각하면 이해하기 편하다.

메서드의 이름이 Did이기 때문에 이미 mount(초기 렌더링)이 일어났다는 의미이다.

컴포넌트가 첫 렌더링된 후

componentDidMount() {

// 작업

}

mount는 전체적인 초기 렌더링을 의미한다.

즉, componentDidMount() 안에 수행하고 싶은 작업을 적으면 초기렌더링 직후에 특정작업(ex> API호출)을 진행한다는 의미이다.

 

🤔 초기 렌더링 이후에 진행한다고?

"그러면 내가 보는 화면은 뭐지?

API가 호출되어야 원하는 값들이 화면에 나올것인데?

초기 렌더링 이후에 작업을 한다면, API로 호출해서 화면에 띄워줄 값들은 어떻게 화면에 바로 나올수가 있지?"

 

내가 라이프사이클을 공부하면서 의아하게 생각했던 부분이었다. 그래서 실행순서를 더 팠던거 같다.

그럼 API를 받아오기전의 화면을 1번 초기렌더링을 이미 하고 ➡️ 그 다음에 API를 호출해서 값을 받아오고 다시 렌더링?인가?

라는 생각이 들었다. 하지만 확실한게 아니기 때문에 실험을 했다.

useEffect안에서 API를 호출할때, setTimeout()을 이용해서 호출시간에 딜레이를 주는 것이다.

 

기존코드

 useEffect(() => {
    fetchData();
  },[]);

setTimeout을 통해 API호출에 딜레이를 준 코드

  useEffect(() => {
     setTimeout(() => {
      console.log("3초가 딜레이됩니다.");
      fetchData();
    }, 3000);
  }, []);

 

딜레이를 주니 아래와 같이 데이터를 받아오기 전에 화면이 처음 렌더링되고(마운트)

➡️ 그 이후에 API로 받아온 값들을 화면에 렌더링하는 것을 알 수 있었다.

딜레이를 주지 않으면 마운트되고, API로 받아와서 화면에 그려주는게 엄청나게 빨리 일어나서 마운트 이후useEffect가 실행된다는 것이 체감되지 않는 것이다.

그래서 눈으로 보여지는 것들 때문에 useEffect의 실행순서를 이해하기가 어려웠었다.

 

 

➡️ useEffect는 기본적으로 마운트이후에 이루어진다.

 

 

✅ componentDidUpdate() 

componentDidUpdate와 useState의 의존성배열은 유사한 역할을 하지만, 정확하게 일치하는 역할을 한다고 할 수는 없다.

따라서 useEffect를 이해하기 위한 것이라면, 이 부분은 자세하게 보지 않고 useEffect의 의존성배열에 대해서 더 공부하는 것이 나을 것 같다.

(비슷한 기능인데 좀 달라서 오히려 헷갈리게 하기도 하는듯하다. 오히려 너무 헷갈릴거같으면 useEffect의 의존성배열부터 읽는 것을 추천한다.)

 

업데이트 (Updating): 컴포넌트의 상태나 프로퍼티가 변경되어 업데이트되는 단계

  • static getDerivedStateFromProps: props로부터 상태를 업데이트
  • shouldComponentUpdate: 컴포넌트의 업데이트를 결정
  • render: 업데이트된 UI를 렌더링
  • getSnapshotBeforeUpdate: DOM 업데이트 직전에 발생하는 작업을 처리
  • componentDidUpdate: 컴포넌트가 업데이트된 후 호출

 

componentDidUpdate도 이름처럼 업데이트이후(재렌더링 이후)에 특정작업을 수행하도록 한다.

만약 여기에 적지 않고 componentDidmount에만 적으면, 마운트시 1번만 수행되도록 하지만, 이 함수안에 적으면 재렌더링되었을때도 특정 작업을 다시 수행하도록 한다.

리렌더링

ComponentDidUpdate(){

//작업

}

클래스형에서는 setState를 통해 상태가 변했을때, 함수형에서는 useState를 통한 set함수를 통해서 상태가 변했을때 상태변화를 감지하고 재렌더링이 일어난다. 그런데 마운트될때 뿐만 아니라, 특정한 값의 변화가 일어날때마다 특정 작업을 수행하야하는 경우가 있다.

 

클래스형에서는 componentDidupdate메서드를 사용했지만, 함수형에서는 useEffect의 의존성배열을 이용한다.

 

componentDidUpdate:

  • 클래스 기반 컴포넌트에서 사용
  • 컴포넌트가 업데이트된 후에 실행되는 라이프사이클 메서드
  • 업데이트 이후에 발생한 작업을 처리하는 데 사용
  • 주로 외부 데이터의 동기화, DOM 조작, 다른 컴포넌트와의 상호 작용에 활용

 

useEffect의 의존성 배열

  • 함수형 컴포넌트에서 사용
  • 컴포넌트의 상태 값 또는 프로퍼티가 변경될 때 컴포넌트를 리렌더링하도록 설정
  • 의존성 배열에 지정된 상태나 프로퍼티가 변경될 때에만 해당 useState 블록이 실행
  • 주로 컴포넌트의 상태 관리 및 렌더링 최적화에 사용

 

둘다 재렌더링이 일어난 이후에 실행된다는 공통점이 있다.

하지만 useEffect의 의존성배열은 배열에 지정함으로써 "이 값이 변경될 때, 특정작업을 실행하라"는 의미를 더 지니고 있다. 

정확히 순서를 적으면, 의존성배열안의 state값이 변경되면 ➡️ 재렌더링이 일어나고 ➡️ useEffect안에 적은 특정 작업이 다시 수행된다.

 

 

만약 위의 의존성 배열을 아예 적지 않는다면, 매번 재렌더링을 하는 상황이 발생하고 (useEffect를 안쓰것과 유사하게 / 대신 수행순서만 마운트이후로 밀린다.)

의존성배열에 빈배열을 넣으면, 맨처음 마운트시에만 특정작업을 수행한다.

 

 

🌫 componentWillUnMount() 

컴포넌트가 제거되기 직전

componentWillUnMount() {

//작업

}

 

함수안에서 수행하게 했던 특정작업을 컴포넌트가 화면에서 없어지는 것(언마운트)를 기준으로 그만두게 한다.

(언마운트 이전 작업 소멸)

 

어떤 작업은 단 한번 수행으로 끝나지만, 어떤 작업은 한번 시작했으면 계속해서 진행되는 것들이 있다.

 예를 들면, setInterval()과 같은 메서드이다. 

 

토글을 누르면 현재 시간을 1초단위로 콘솔에 찍히게 했다. 문제는 이 코드는 꺼주지 않으면 계속해서 작업을 수행한다는 것이다.

그래서 이때 사용하는 것이 클래스형에서는 componentWillunmount, 함수형에서는 클린업 함수이다.

 

컴포넌트가 화면에서 사라 질때 (정확하게는 브라우저 화면에서 사라지기 직전)에 클린업함수는 이전에 수행했는 작업을 정리한다.

 

return 값으로 구독을 해지해주는 코드를 넣으면 된다.

예를들어 setInterval()을 구독해지하려면, return 안에서 clearInterval()을 한다.

 

그리고 토글을 통해 클릭시 화면에서 사라지게(unmount) 만든다.

화면에서 토글이 사라짐과 동시에 setInterval이 멈추고 콘솔창에 계속해서 찍히는 것도 멈춘다.

 

unmount만 사라지게 직전에 useEffect안의 코드가 먼저 수행된다.

(그래서 이름이 componentWillunmount!!)

 

🤔 컴포넌트가 사라지는 모든 경우에 클린업 함수가 실행되는 것은 아니다?

클린업함수를 실행해보려고 이런 저런 실험을 해보았는데, 컴포넌트가 사라지는 경우라고 해서 모두 클린업 함수가 실행되는건 아니었다.

 

예를 들면, 이미지 태그에 window.locaion.pathname으로 클릭시 search페이지로 이동하게 만들었을때는 클린업 함수가 실행되지 않았다.

onClick={() => (window.location.pathname = "/search")}

그런데 리액트 라우터의 <Link>를 통해서 똑같이 클릭시 search페이지로 이동하게 했더니 이번에는 클린업 함수가 실행되었다.

 

url 변경을 통해서 직접 경로를 이동해도 클린업 함수가 동작하지 않았다.

클린업 함수가 동작하게 하는 언마운트가 있고, 그렇지 않은 언마운트도 존재하는 것 같아서 이 부분은 앞으로 계속 코드를 짜면서 더 공부해봐야 할 것 같다.

 

 

🔫 클래스형이 아닌 useEffect 훅을 사용하는 이유

1️⃣ useEffect는 3개 합친것이고 여러번 사용가능하다.

그래서 state마다 다른 이펙트를 낼 수 있다.

 

-클래스형에서는 한번에 처리 (메서드는 많다...)

componentDidMount() {
this.setState({
imgCord: 3,
score:1,
result:2,
})
}

-useEffect는 나눠서 처리가능 (useEffect하나로 가능)

useEffect(() => {
setImgCord();
setScore();
},[imgCord,score])

useEffect(()=>{
setResult()'
},[setResult])

 

 

 

2️⃣ 클래스형에서는 여러가지 메서드들을 섞어서 사용해서 라이프사이클을 관리했다면,

훅에서는 useEffect 하나로 관리한다. (편리)

함수형에서는 원래 라이프사이클 관리가 어려웠지만, 각종 hook들이 나오면서 라이프사이클 관리가 가능해졌다.

 

 

 

 

🖊 요약

useEffectcomponentDidMount()  + ComponentDidUpdate() + componentWillUnMount()의 기능을 합친 것.

(물론 완전 동일은 아니지만, 거의 동일)

 

useEffect의 실행순서마운트이후, 재렌더링 이후, 언마운트 이전

 

➡️ 특정 작업을 컴포넌트 렌더링을 기준으로 언제 실행될지를 지정할 수 있다.

➡️ 클래스형보다 쉽게 라이프사이클 관리가 가능

 

 

 


✚ 더 자세한 설명

클래스의 라이프사이클 실행순서

constructor => 렌더링 => ref => componentDidMount => (setState/props바뀔때 => sholudComponentUpdate(true) => 리렌더링 => componetnDidUpdate) 

// 부모가 나를 없앴을 때 componentWillUnmount => 소멸

 

 

컴포넌트가 ReactDOM.render를 통해서 렌더링되는 순간, constructor부분이나 만든 메서드들이 다 클래스에 갖다 붙는다.

다 붙으면 렌더링을 처음으로 한번 한다. (마운트)

(혹시 ref가 있으면 ref까지 처리)

 

첫번째 렌더링이 끝났다면, componentDidmount가 실행된다.

 

컴포넌트에서 setState하거나 부모가 props를 바꿀때 리렌더링이 일어난다.

리렌더링이 일어날때 shouldComponentUpdate

리렌더링 후에는 componentDidUpdate

부모가 나를 없앤후에는 componentWillunmount

 

 

함수형 컴포넌트의 라이프사이클 실행순서

 

마운팅

컴포넌트를 마운팅 => 리턴 값을 렌더링 (가상돔) => 렌더링된 상태를 가상돔에 그림 => 가상돔과 돔의 element들을 비교 =>

만약 가상돔의 element들과 리얼돔의 element들 사이에 다른 점이 감지된다면 그 부분만을 돔에 업데이트 , 초기 렌더링에 경우 현재 컴포넌트에 대한 정보가 돔에 없기때문에 현재컴포넌트의 모든 부분을 돔에 그린다. => useEffect, useLayotEffect 여기까지가 마운팅과정

 

updating

직전에 실행된 useEffect나 유저의 인터렉션등에 의해 state / props값이 변경 => 가상돔을 변경된 state / props값에 맞추어 다시 그린다. => 가상돔을 돔의 element들과 비교 => 만약 가상돔의 element들과 돔의 element들에서 다른 부분이 감지된다면 그 부분만을 돔에 업데이트 (state나 props의 값이 변경되었지만 UI에 변화가 없다면 돔을 업데이트하지 않는다.) => useEffect, useLayotEffect훅을 실행 여기까지가 업데이팅과정

 

컴포넌트가 소멸할 때 useEffect 내부에 리턴에 정의된 클린업 함수를 실행 => 컴포넌트 소멸 


 

👩‍💻 내 생각

useEffect의 정확한 실행시점과 내부어떻게 돌아가는지가 궁금해서 공부하다보니, 브라우저 렌더링과정과 리액트 라이프사이클 과정도 깊게 공부하게 되었다. (브라우더 렌더링 과정도 포스팅할 예정)

 

파다보니 자꾸 더 알게되는 내용이 많아지고.. 그러다보니 내용이 엄청 길어졌는데, 결국에는 저 요약 부분이 제일 중요한 내용인것같아서 한번 이해가 된 이후에는 저 요약 몇줄만 보면 될 것 같다.

 

브라우저 초기렌더링이후에 데이터를 받아오는건데 속도가 엄청 빨라서 몰랐구나..이 부분이 엄청 신기했다 ㅎㅎ

useEffect의 실행순서와 클래스형과 관련한 내부가 어떻게 돌아가는지 위주로 정리했다. 

브라우저 렌더링과정과 useEffect에 대해서도 한번 더 공부하고 포스팅 할 예정이다.

(공부하다보니 알게 되는게 너무 많다..!!)

 

 

 

참고자료

https://youtu.be/aUXwUqgYREI?si=DOYibDs5pK2-H9la

https://youtu.be/ltw4FYagLfM?si=h5jm4_Y8VSna8KH1

https://youtu.be/kyodvzc5GHU?si=bujuhFm0wQIw6tOY 

https://react.vlpt.us/basic/25-lifecycle.html