[React 이모저모] 리액트 Hook의 작동흐름

yesonn.log·2023년 4월 30일
0

리액트

목록 보기
1/1

Hooks란?

리액트에서 기존 사용하던 Class를 이용한 코드를 작성할 필요 없이 state와 여러 React 기능을 사용할 수 있도록 만든 라이브러리

Hooks 등장 배경

클래스형 컴포넌트 vs 함수형 컴포넌트

React v0.14.0부터 함수를 사용한 컴포넌트를 만들 수 있게 되었다. state나 라이프사이클 메서드가 필요한 컴포넌트는 클래스형 컴포넌트를, props를 받기만 하거나 UI렌더링 코드만 있는 컴포넌트는 함수형 컴포넌트를 사용해 만들 수 있다.

클래스형 컴포넌트의 단점

자바스크립트의 this는 다른 언어의 this와는 달리 런타임에 의해 결정되는데 이렇게 컨텍스트에 따라 this가 바뀐다는 자유성이 오히려 바인딩 면에서 단점으로 비춰지는 경우도 많다.
위와 같은 클래스형 컴포넌트의 단점을 해결하고자 함수형 컴포넌트에서도 state와 라이프사이클 메서드 관리 기능을 사용할 수 있는 Hooks가 도입되게 된 것이다.

대표적인 Hooks로는 useState, useEffect,useReducer, useMemo 등의 Hook들이 존재한다.

해당 아티클은 이러한 리액트 Hook의 실행흐름에 대한 궁금증을 해소했던 경험을 토대로 간단히 짚어보고자 한다.

Hook, 특히 이펙트 클린업에 대해 공부하던 중 다음과 같은 대목이 있었다.

Clean-up

"이펙트의 클린업은 "최신" prop을 읽지 않는다.
클린업이 정의된 시점의 랜더링에 있던 값을 읽는다."

이게 무슨 말일까?
도무지 이해가 되지 않아 이해시키고 싶었다.

그러니 예시 코드를 통해 이해시켜보자.

useEffect(() => 
{ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
return () => {ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
  };
});

위와 같은 코드에서 첫 번째 랜더링에서 prop 이 {id: 10} 이고, 두 번째 랜더링에서 {id: 20} 이라고 가정해보자.

그렇다면 과연 어떤 흐름대로 실행될까?

  • 리액트가 {id: 10} 을 다루는 이펙트를 클린업한다.
  • 리액트가 {id: 20} 을 가지고 UI를 랜더링한다.
  • 리액트가 {id: 20} 으로 이펙트를 실행한다.

나는 당연히 위와 같은 흐름으로 작동될 것이라 예상했다.

만약 위와 같은 흐름으로 작동된다면, 클린업이 리랜더링 되기 전에 실행되고 이전의 prop을 “보고”, 그 다음 새 이펙트가 리랜더링 이후 실행되기 때문에 새 prop을 “보게" 되는 것 아닌가?

머리가 점점 복잡해졌다.


.

.

.

실제로 리액트 훅은 이렇게 작동하지 않는다.
.
.

왜냐!!
리액트는 브라우저가 페인트 하고 난 뒤에야 이펙트를 실행한다. 그렇게 해야 대부분의 이펙트가 스크린 업데이트를 가로막지 않기 때문에 앱을 빠르게 만들어준다고 한다.
마찬가지로 이펙트의 클린업도 미뤄지며 이전 이펙트는 새 prop과 함께 리랜더링 되고 난 뒤에 클린업되는 작동 구조를 보이고 있다는 사실!

위 사진과 같은 작동 흐름을 위 코드 예시에 적용해보면 실제 흐름은 다음과 같다.

  • 리액트가 {id: 20} 을 가지고 UI를 랜더링한다.
  • 브라우저가 실제 그리기를 한다. 화면 상에서 {id: 20} 이 반영된 UI를 볼 수 있다.
  • 리액트는 {id: 10} 에 대한 이펙트를 클린업한다.
  • 리액트가 {id: 20} 에 대한 이펙트를 실행한다.

컴포넌트가 랜더링 안에 있는 모든 함수는 (이벤트 핸들러, 이펙트, 타임아웃이나 그 안에서 호출되는 API 등) 랜더가 호출될 때 정의된 props와 state 값을 잡아둔다.

따라서 이펙트의 클린업은 "최신" prop을 읽는 것이 아닌, 클린업이 정의된 시점의 랜더링에 있던 값을 읽게 되는 것이다.
어떻게 보면 그냥 리액트의 작동 흐름 자체가 이렇기 때문에 위와 같은 특징을 보이는 것이라고도 쉽게 이해해볼 수 있을 것 같다.

// 첫 번째 랜더링, props는 {id: 10}
functionExample() {  
// 
...useEffect(   
 // 첫 번째 랜더링의 이펙트   
 () => {ChatAPI.subscribeToFriendStatus(10, handleStatusChange);     
 // 첫 번째 랜더링의 클린업 (클린업이 정의된 시점의 랜더링에 있던 값을 잡아둠)
return () => {
ChatAPI.unsubscribeFromFriendStatus(10, handleStatusChange);      
};    
}  
);  
// 
...}
// 다음 랜더링, props는 {id: 20}
functionExample() { 
 // ...useEffect(   
 // 두 번째 랜더링의 이펙트   
 () => {ChatAPI.subscribeToFriendStatus(20, handleStatusChange);      
// 두 번째 랜더링의 클린업
return () => {
ChatAPI.unsubscribeFromFriendStatus(20, handleStatusChange);      
};    
}  
);  
// 
...}

따라서 이렇게 리액트는 페인팅 이후 이펙트를 다루는 것이 기본이며, 그 결과 앱을 빠르게 만들어줄 수 있다!

lifecycle이 아니라 동기화

리액트는 우리가 지정한 props와 state에 따라 DOM과 동기화한다.
즉, 렌더링 시 "마운트"와 "업데이트"의 구분이 없는 것이다.

이건 또 무슨 말이냐고?
이것이 헷갈리는 이유는 우리는 컴포넌트 라이프사이클에서 컴포넌트가 업데이트 또는 마운트된다고 배웠기 때문일 것이다.

하지만 실제 리액트는 그 두 개를 구분하지 않는다고 한다.
충격적!

리액트는 그저 자신이 관리하는 컴포넌트들을 실제 DOM에 시각적으로 동기화할 뿐이다.
이펙트도 같은 맥락이다.

useEffect의 진짜 목적은 리액트 컴포넌트 트리 바깥에 있는것들을 props와 state에 따라 동기화하게 되는 것이다.

나와 같은 리액트의 작동 흐름에 대한 단순 의문, 혼란을 지니고 있던 분들에게 조금이나마 도움이 되는 글이 되었길!

profile
개발뿡나무 성장일지

1개의 댓글

comment-user-thumbnail
2023년 5월 6일

꺄락! 리액트의 작동 흐름에 대해서 명확히 잡을 수 있었어요! 사실 아직도 클래스형 컴포넌트에 대한 글들이 남아있을 만큼 훅이 나온 게 최근인데, 그만큼 리액트가 빠르게 변화하고 있다는 생각도 드네용ㅎㅎ

답글 달기