😎 함수형 컴포넌트와 클래스형 컴포넌트의 차이?
열심히 구글링을 하며 웹서핑을 하던 중 흥미로운 게시글을 발견했습니다.
https://overreacted.io/ko/how-are-function-components-different-from-classes/
Dan Abramov이란 redux, Creat React App의 공동저자이신분이 작성한 게시물인데
클래스 컴포넌트와 함수형 컴포넌트의 차이를 좋은 예시를 통해
길게 설명을 해주는 게시물입니다.
단순히 함수형은 라이프사이클 API를 사용할 수 없다거나 hook을 사용한다거나 하는 것이 아닌
명확한 차이를 설명해주어서..
아주 유익했지만.. 제 머리로는 쉽게 이해하기 힘들었으므로 정리해보겠습니다.
실제 게시물을 읽어보면 CodeSandBox사이트를 통해
직접 실행도 해볼 수 있게 되어있으니 꼭 방문해보시는 걸 추천드립니다.
추가로 제 해석은 틀릴 수 있습니다... 저 혼자의 생각이기때문에
혹시 제 글에 오류가 있을 시 지적해주시면 감사하겠습니다.
😎 위 게시물에서 주장하는 가장 큰 차이는?
this의 사용유무가 관건이라고 요약할 수 있을 것 같습니다.
위 게시물에서 중점적으로 주장하는 내용은 다음과 같습니다.
함수형 컴포넌트는 렌더링 값을 고정시킨다.
그 이유는 함수형 컴포넌트는 통상적으로 this를 사용하지 않고 인자를 받아서 사용하기 때문이다.
전 이걸 이해하는 과정에서 arrow function,이벤트루프,콜스택,this에 대한 지식이 필요했습니다.
아 그냥 그렇구나 this는 변하는구나~하고 지나가도 무방하겠지만
굉장히 재밌는 내용이었습니다. 여러분들도 부디 즐겨주세요
추가로 원문에 이미 명쾌하게 설명이 되어있는 부분은 굳이 설명하지 않겠습니다.
😎뭐가 문젠데?
https://codesandbox.io/s/pjqnl16lm7
위 링크의 예제를 캡쳐해온 사진입니다. 실행시켜보는게 이해가 더 빠르지만
실행시켜볼 환경이 안되시는 분들을 위해 설명을 드리자면 다음과 같습니다.
import React from 'react';
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}
export default ProfilePage;
이 코드는 클래스형 컴포넌트로 작성되었습니다.
쉽게 과정을 설명하자면 다음과 같아요
1.버튼을 클릭하면
2.setTimeout함수를 실행하고
3.타이머가 3초가 지나서 다 돌아가면 showMessage 메서드를 호출하고
4.showMessage 함수를 호출하면 this.props.user 정보를 보여주는
동작을 하는 코드입니다.
그런데 만약에 setTimeOut함수가 실행되고 3초가 지나는 저 2-3 과정간의 3초의 시간동안
props.user의 정보가 변경되면 어떻게 될까요?
Dan을 follow하고 3초가 지나기전에 Sophie의 프로필로 옮겨간다면?
클래스 컴포넌트의 this.props.user는 sophie를 표시해줍니다.
근데 우리가 follow를 하는건 follow를 클릭한 당시의 사람을 하고싶은건데
follow하고 다음 사람으로 넘어갔더니 그 사람이 팔로우되면 어이없잖음
이건 왜 그런걸까요?
😎 원문의 설명
var value = 1;
const ojb = {
value: 100,
foo() {
console.log("foos this:", this); // value:100 , f
setTimeout(function () {
console.log("callback this : ", this); // global
console.log("callback this value", this.value); // undefined
}, 100);
},
};
ojb.foo();
사실 지식이 풍족하신 분들은 위 설명만 보아도 쉽게 이해가 되시겠지만..
저는 종속관계가 깨져버렸다는 표현에 꽂혔습니다.
this는 가변적이다. 따라서 리렌더링되면 this.props도 바뀐다
showMessage 메서드는 새로운 (다시 렌더링 된) props의 user를 읽는다.
그렇기때문에 2-3과정의 3초의 공백에서 클래스 컴포넌트가 리렌더링되면
(즉 Dan의 프로필에서 Sophie의 프로필로 옮겨가면) 다시 렌더링이 되고
this는 다시 렌더링 된 props의 유저를 띄워준다는 것입니다.
그런데 여기서 의문이 생깁니다.
https://xionwcfm.tistory.com/142
위 링크는 제가 모던 자바스크립트 딥다이브를 읽으며 this에 대한 내용을 정리한 글입니다.
위 예제에서 메서드 내의 중첩함수 setTimeout은 콜백 함수로
일반 함수를 호출하게 되면 bind가 되지 않습니다.
이것을 해결하기 위해서 할 수 있는 방법은 크게 세가지가 있습니다.
1.변수할당
2.bind 메서드 사용
3.arrow function의 사용
즉 중첩함수를 일반함수로 호출하면 this에는 전역이 바인딩 됩니다.
하지만 setTimeout함수의 콜백함수 this.showMessage는
arrow function을 통해 바인딩을 해주었음에도 불구하고
위 예제에서는 this 값을 너무 늦게 읽어오는 탓에 의도대로 출력이 되지 않습니다.
그러니까 문제는 this가 바인딩이 안되어서 값이 안나오는게 아니라는거죠!
만약 중첩함수 this 바인딩이 잘못되어서 우리의 실행이 의도대로 동작하지 않는거라면
리렌더링된 user의 값이 나오는게 아니라 undefined가 반환되어야할테니까요
😎종속관계가 왜?
그러면 대체 왜 this가 바인딩을 한 props를 받아서 동작하지 않고
어째서 새로 렌더링 된 props를 받아와서 그걸 띄워주는걸까요..?
아니 바인딩을 해줬는데 왜 안됨?
이렇게 명시적으로 this를 바인딩해주어도 아무것도 해결할 수 없습니다.
사실 저 바인딩하는 과정을 넣고 메서드 표현식으로 표현을 하는거나
애로우 펑션으로 메서드를 표현하는거나 this의 바인딩 관점에서는 똑같은 행동이니까요!!
중요한건 중첩함수가 참조를 찾아가지 못하는 게 아니라
this가 가변적으로 변하는데 원래의 참조가 끊겨서 엉뚱한 곳을 참조한다는것에 있습니다.
대체 왜..?
😎 여기서부터는 제 뇌피셜입니다.
이벤트 루프는 콜스택이 비워질때까지 태스크큐에 비동기함수의 콜백함수를 보관합니다.
그리고 전역실행컨텍스트까지 실행을 완료하고 pop 되어 콜스택이 완전히 비워지면
마이크로태스크큐 -> 애니메이션프레임 -> 태스크큐 순으로 이벤트루프를 통해 콜스택으로 옮겨주며 실행시킵니다.
그런데 try..catch와 같은 에러전파를 공부할 때 비동기 함수의 에러처리 한계에 대한 문제를 봤습니다.
내용인 즉 다음과 같습니다.
에러는 호출자 방향(콜스택의 아래방향) 으로 전파되지만 비동기 함수의 콜백함수는
호출자가 명확하지 않기 때문에 에러를 캐치할 수 없다.는 내용이었습니다.
그런데 그렇다면 이와 같은 문제가 this에서도 발생하는 것 아닐까요?
왜냐면 this는 어디에 정의되었느냐가 아닌 "누가 호출하였는가"를 중심으로 동작이 바뀌는데
비동기 함수의 this는 호출자가 명확하지 않으니까요
먼저 ChatGPT에게 비동기함수의 콜백함수가 참조하는 this에 대해 물어봤습니다
답변을 번역기에 돌린 결과는 다음과 같습니다.
예, 맞습니다.
비동기 함수가 호출되면 해당 콜백 함수는 이벤트 큐에 배치되고 호출 스택이 비워질 때까지 실행되지 않습니다.
이때 콜백 함수 내부의 this 값은 콜백이 정의된 시점이 아니라
콜백이 실행된 시점의 호출 사이트에 의해 결정됩니다.
따라서 this의 값이 콜백 함수 내에서 명시적으로 설정되지 않은 경우(예: bind(), call(), apply()
또는 화살표 함수 사용)는 모호할 수 있으며 의도한 개체를 참조하지 않을 수 있습니다.
이를 방지하려면 비동기 콜백 함수에서 this 값을 명시적으로 설정하는 것이 좋습니다.
일단 chatGPT의 말에 의하면
this의 값이 콜백 함수 내에서 화살표 함수 사용으로 인해 명확히 설정되지 않으면
의도한 개체를 참조하지 않을 수 있다고합니다.
그렇다면 종속관계가 깨진다는 것에 대한 말은 이벤트루프와 콜스택과 밀접한 관련이 있는게
맞을 것 같습니다.
다시 한번 chatGPT에게 질문을 했습니다.
내용은 다음과 같습니다.
React의 클래스 기반 구성 요소에서 this는 구성 요소의 인스턴스를 나타냅니다.
비동기 콜백 함수가 작업 대기열에서 대기하는 동안 다시 렌더링이 발생하면
구성 요소의 인스턴스가 변경되었을 수 있으며
이는 구성 요소가 처음 마운트될 때 생성된 원래 인스턴스가 아닌 새 인스턴스를 참조합니다.
콜백 함수가 오래된 상태 또는 소품을 참조하거나 의도한 인스턴스 메서드에 액세스할 수 없기 때문에
예기치 않은 동작이 발생할 수 있습니다.
이를 방지하려면 비동기 콜백 함수 내부의 this 값이 명시적으로 원하는 객체로 설정되었는지
확인하는 것이 중요합니다.
React의 클래스 기반 구성 요소에서 이것은 종종 생성자에서 화살표 함수 또는 bind()를 사용하여 달성됩니다.
정확히 원하던 답변은 아니지만 대략.. 원래 인스턴스가 아니라 새 인스턴스를 참조한다.
또한 콜백 함수는 오래된 상태를 참조할 수 없다.라고 하는군요
React의 클래스 기반 구성 요소에서 this는 구성 요소의 인스턴스를 나타냅니다.
구성 요소가 다시 렌더링되면 구성 요소의 새 인스턴스가 생성됩니다.
비동기 콜백 함수가 작업 대기열에서 대기하는 동안 리렌더링이 발생하면 콜백 함수는
리렌더링 이후에 실행되며 이때 생성된 원래 인스턴스가 아닌 구성 요소의 새 인스턴스를 참조합니다.
구성 요소가 처음 마운트되었을 때. 이는 콜백 함수가 오래된 상태 또는 소품을 참조하거나
다시 렌더링하는 동안 변경되었을 수 있으므로 의도한 인스턴스 메서드에 액세스하지 못할 수 있기 때문에
예기치 않은 동작으로 이어질 수 있습니다.
이를 방지하려면 비동기 콜백 함수 내부의 this 값을 원하는 객체(예: 구성 요소의 원래 인스턴스)로
명시적으로 설정하여 의도한 상태 및 메서드를 참조하도록 하는 것이 중요합니다.
chatGPT에게 좀 더 자세히 물어보니 작업 대기열에서 대기하는 동안
리렌더링이 발생하면 콜스택에 리렌더링에 대한 처리가 들어오니까
리렌더링까지 마친 후에 실행되고 이때 새 인스턴스를 참조한다고 하는군요
우리의 문제는 this를 너무 늦게 읽는다는 것이었습니다.
그 이유는 이것 때문이군요!
태스크 큐에 대기하는 동안 리렌더링이 발생하면 리렌더링을 다 한 다음에 실행되니...
저자는 이문제에 대한 해결책으로 변수에 미리 상태를 저장해두는것과
클로저를 활용하는 것을 추천합니다.
변수에 미리 상태를 저장해두는 건 쉽게 떠올릴 수 있지만
클로저로도 해결이 된다..?
class ProfilePage extends React.Component {
render() {
// props의 값을 고정!
const props = this.props;
// Note: 여긴 *render 안에* 존재하는 곳이다!
// 클래스의 메서드가 아닌 render의 메서드
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return <button onClick={handleClick}>Follow</button>;
}
}
참조할 변수가 콜스택이 비워지고 난 후에도 살아남아 있다면(클로저라면)
비동기의 콜백함수는 참조를 타고 올라가다 그 클로저를 제일 먼저 마주치게되고 그 클로저의 값을 사용한다는 것?
그렇기 때문에 클로저라면 문제가 없다는 거군요
😎 결론이 뭐임?
비동기 함수의 콜백함수와 리렌더링이 조합되면 예기치 못한 동작을 일으킨다.
그리고 이것은 this가 가변적이라는 사실과 이벤트루프와 밀접한 연관이 있다.
해결할 기법을 도입하면 클래스형 컴포넌트에서도 의도대로 동작시킬 수 있지만
그냥 함수형 컴포넌트 쓰면 쉽게 해결이 된다 ㅎㅎ;;;
'react' 카테고리의 다른 글
useReducer를 케이크처럼 쉽게 이해하는 법 (1) | 2023.03.13 |
---|---|
React 프로젝트 시작 전 환경 세팅하기 (0) | 2023.03.01 |
리액트 엘리먼트 렌더링이란? (0) | 2023.02.03 |
함수 컴포넌트와 useState (1) | 2022.12.28 |
클래스형 컴포넌트와 state (0) | 2022.12.28 |