[React] class 컴포넌트 vs 함수 컴포넌트 내부구조 들여다보기 + hook

devAnderson·2022년 4월 11일
0

TIL

목록 보기
86/103

🚍 0. 원래는 훅에 대해서 공부하려던 것이었는데

요즘 면접준비를 하면서 react 관련 내용을 살펴보다가 hook 에 대해서 설명해야 하는 부분이 있었다.

일전 공식사이트에서 내용을 살펴본 기억은 있었다.

뭐라 내용이 많다만

나는 해당 내용을 이렇게 이해했다 (우선 우리는 hook 이 없는 세계의 함수 컴포넌트를 사용한다고 생각해보자)

만약 우리가 어떤 컴포넌트를 재사용해야 한다면 이 컴포넌트는 필연적으로 props에 종속적인 형태가 될 수밖에 없다 ( presentational component를 연상하자 )

정적으로 고정된 상태만 보여줄 것이 아니라면, 변동되는 상태를 관리해야 하는데 이것이 함수 컴포넌트에서는 오롯이 props로만 가능하기 때문이다. (useState 마렵다)

근데 이런 형태의 컴포넌트는 결국 지독하리만큼 깊어지는 props의 헬을 겪을 수밖에 없다.
굳이 그 자체의 컴포넌트 내부에서만 사용되며 관리되는 상태가 필요할 뿐인데 그것을 위해서 부모로부터 계속 props를 건네주는 것은 재사용에도 옳지 않고 부모에게 종속적이게 만든다.

따라서 이를 해결할 방법이 필요했다고 그들은 설명한다.

둘째로, hook을 도입하게 된 계기를 이렇게 말하고 있다.

본디 class에서는 상태관리를 위한 메서드들(ex, componentDidMount)을 제공해주고 있다.

이 메서드들은 개발자의 의도대로 호출할 수 있기는 하지만 이것을 직접적으로 관리하는 것은 몹시도 번거롭고 중복되는 코드가 늘어나고 버그를 야기한다.

영상에서도 보면 어떻게 하면 한번에 상태주기를 hook으로 해결하는지를 보여주는 퍼포먼스를 감상할 수 있다. hook이 있으면 이렇게 달라집니다

그래서 나는 대략 hook이 뭔지에 대해서 설명하라고 하면 이렇게 말하게 말하고 싶어진다

hook이란, 함수 컴포넌트 내에서 상태로직을 독립적이고 설정할 수 있도록 도와주어 재사용이 용이하도록 하거나, 상태 주기 함수들을 추상화하여 코드를 줄이고 직관적으로 작성할 수 있도록 도와주는 함수들의 집합이다.

근데, 정리를 하다보니 갑자기 class와 function 컴포넌트의 차이가 알고싶어서 정리를 해보려고 한다.

그리고 추가적으로 react18에서 말하는 root의 개념도 살펴보려고 한다.

🚍 1. Class와 function 컴포넌트특징

Class 는 es6부터 도입된 자바스크립트의 객체 지향적 개발을 위한 문법적 설탕이다.
기존에 생성자 함수로 객체를 만들던 과정을 최대한 단순화하고, 손쉽게 만들 수 있도록 변화시켰다.

class문법에서 가장 눈여겨 봐야 할, 그리고 react의 class 컴포넌트를 이해하기 위한 중요한 파트가 바로 "extends"일 것이다.

사실상 extends는 뭔가 엄청 특별한 것이 아니라, 기존에 있던 prototype의 상속개념을 조금 더 추가적으로 복잡하게 만든 것이다.

class Parent {
    pMethod(){};
}

class Child extends Parent {
    cMethod(){};
}

위와 같이 상속을 실행했을 경우, 자바스크립트 엔진의 진행과정은 다음과 같다.

  1. 생성자 Parent 함수를 평가하여 함수객체화한다. 이때 prototype 프로퍼티가 설정이 된다

    prototype : { constructor, pMethod, [[prototype]]}
    이때 prototype에 할당되는 객체의 [[prototype]] 슬롯에 들어갈 객체는 Object.prototype이다.

  2. 생성자 Child 함수를 평가하여 함수객체화한다. 이때 prototype 프로퍼티가 설정이 된다

    prototype : { constructor, cMethod,[[prototype]]}
    NOTE 이때, prototype에 할당되는 객체의 [[prototype]] 슬롯에 들어가는 것은 extends대상인 Parent.prototype 이다.
    NOTE 또한, 생성자 Child는 함수객체이므로 내부에 [[prototype]] 슬롯이 존재한다. 특이한 부분이, 여기에 할당되는 것 은 Function.prototype이 아닌 Parent.prototype이다.

prototype 프로퍼티에 할당되는 객체의 [[prototype]] 슬롯에 extends 대상인 Parent.prototype이 들어감으로써 프로토타입 체인이 구현되어 있음을 알 수 있다.

그런데 정작 생성자 함수객체의 (그러니까 class Child의 [[prototype]] 슬롯에 역시 extends 대상인 Parent.prototype이 들어가는 걸까?

간단하게 생각해보면 인스턴스가 아닌, 함수 객체 그 자체에서 정적으로 사용할 수 있는 메서드들을 집어넣기 위한 의도로 보인다.

여튼, class 문법을 통해서 확인할 수 있는 것은 암묵적으로 생성되는 prototype 프로퍼티에 extends 대상의 prototype 프로퍼티 내용이 들어가는 것임을 알 수 있었다.

만약 extends 없이 함수 생성자로 표현하려면 부모에 또 prototype을 정의해줘야하는 줄이 추가됬을것이다. 상속을 아주 간편하게 구현해놓았다는 것을 확인할 수 있었다.

리엑트의 class 컴포넌트 역시 단도직입적으로 말하면 클래스 함수이다.
이 함수 역시 extends를 받는데, 보통 받아오는 생성자 함수가 Component이다.

import React, { Component } from "react";

export default class Test extends Component {
  render() {
    return <div>Test</div>;
  }
}

위에서 봤던 것처럼, extends를 했다는 소리는 해당 함수의 prototype 프로퍼티에 들어가는 객체의 [[prototype]] 슬롯부에 들어가는 내용이 Component.prototype이 된다는 소리이다.

그렇다면 상속을 통해서 확인할 수 있는 Component.prototype의 내용을 간단히 살펴보면

오잉? 뭔가 우리에게 익숙한 것이 보인다 "state, setState"

해당 함수가 모듈을 통해서 나오면, 클로져 형태로 감싸져서 바깥으로 나가게 된다.

이 모듈 시스템은 node에서 볼 수 있는 commonJS의 require 함수가 리턴하는 객체와 매우 유사한 형태를 보여준다.
( 사실 그 내부의 스코프 체인을 들여다보면 상당히 복잡해서 지금은 패스하고 다음에 탐구해볼 생각이다 )

위에서 확인했던 것 대로라면, 해당 클래스 컴포넌트 함수를 console.dir로 찍어보면 그 내부 객체구조에는 분명히 이렇게 되었을 것이다.

  • 해당 평가완료된 생성자 함수객체의 [[prtotype]] 슬롯에는 Component.prototype이 있을 것이다
  • 해당 생성자 함수객체의 prototype 프로퍼티에 할당되는 객체의 [[prototype]] 슬롯 역시 Component.prototype이 있을 것이다.

예상했던 대로의 결과임을 알 수 있었다.
특이한 점은, 해당 컴포넌트가 모듈로 튀어나올 때에 prototype으로 전달되는 값이 몇개 있는데 "isMounted", "replaceState"와 같은 프로토타입 프로퍼티가 따로 정의됨을 볼 수 있다.

여튼, 중요한 것은 모듈 형태로 노출된 생성자 함수는 이제 리엑트 앱에 의하여 호출되고 실행컨텍스트를 만든 후, 렉시컬 환경을 조성하면서 결과적으로 리엑트 객체를 리턴하게 될 것이라는 점이다.

타입스크립트의 정의를 통해 확인할 수 있는 클래스 컴포넌트가 new를 통해 인스턴스를 생성할 때 초기화되는 대상들은 아래와 같다

  1. 우선적으로 클래스에 extends 대상이 있을 경우 인스턴스 생성을 해당 대상에게 넘겨준다
  2. 대상자는 자신이 가진 this.~ 의 문들을 통해 인스턴스를 초기화시킨다. (클래스 제안으로 만들어진 축약형의 경우라면 그냥 할당문처럼 써진 부분)
  3. 그리고 제어권은 다시 피상속자에게 넘어온 후, 피상속자의 몸체에 정의된 코드대로 인스턴스 초기화를 마무리한다. [[prototype]] 슬롯에는 자신의 prototype 프로퍼티의 값이 들어가게 된다 ( 이 내부에는 평가 시점에서 이미 extends에 의한 프로토타입 체인이 완성되어 있었음 )

사실 리엑트를 사용하면 바로 new를 통해 실행되는 게 아니라, jsx가 자바스크립트가 이해할 수 없는 언어이기 떄문에 바벨을 통해서 한번 createElement로 전환하여 리엑트객체를 만들어준 뒤, Render을 하는 시점에서 new를 통해 객체를 만들고 그렇게 만들어진 virtualDOM으로 실제 DOM을 업데이트한다.

클래스는 이처럼 상속을 통해서 state가 내장되어 있기 때문에 컴포넌트별로 독립적인 상태를 사용할 수 있다.
하지만 함수 컴포넌트는 상속을 사용할 수 없기 때문에 컴포넌트별로 독립적으로 상태관리를 할 수가 없었다.

이 문제를 react팀은 16버전으로 올라오면서 업데이트한 hook을 통해서 해결하였다.

즉, class 컴포넌트의 "프로토타입을 기반으로 하는 상태" 관리를 react팀은 hook이라는 클로져를 이용하여 스코프체인을 통해 구현한 것이다. 그리고 이것이 가장 결정적인 class와 function 컴포넌트 둘의 차이라고 보인다.

내부 원리는 더 복잡하겠지만, react에서 제공하는 모듈 스코프 내에 정의된 "useState"가 리턴하는 배열의 두번째 인자는 클로져이기때문에 자신이 정의되어 있는 외부 실행 컨텍스트의 렉시컬 환경을 [[Environment]] 슬롯에 바인딩하고 있는 상태이다. (setState)

이 setState을 호출함으로 인해 리엑트 앱 내의 렉시컬 환경 상에서 관리되고 있는 state값이 변경되고, 이 변경사항을 파악하면 자동으로 componentShouldUpdate가 호출되면서 true를 반환하고,

이로 인해서 render가 다시 실행되는 구조로 이해한다.

2. 결론

클래스 컴포넌트는 상태 관리를 상속을 통해 전달받은 state 프로퍼티와, 프로토타입 체인에 의해 정의되어 있는 setState을 통해서 이루고,

함수 컴포넌트는 상태관리를 훅이라고 하는 클로져를 기반으로 스코프 체인에 의하여 변경시키는 방향으로 관리를 한다.

profile
자라나라 프론트엔드 개발새싹!

0개의 댓글