[Notion Cloning] 회고 01 - Component에 대한 고찰을 바탕으로

devBuzz142·2022년 4월 22일
0

VanillaJS_Component

목록 보기
1/2
post-thumbnail

Component

What is Component?

프로젝트에 앞서 Component란 무엇인가? 에 대해서 고민을 하였다. 위키백과에서 컴포넌트에 대한 정의를 찾아볼 수도 있지만, 지금 하려는 프로젝트에 적용하기엔 다소 원론적인 얘기였다. 이에 현재 프론트엔드 프레임워크, 그 중에서도 컴포넌트 기반 프레임워크의 대표 주자인 리액트의 공식문서를 통해 컴포넌트에 대해 알아보았다.

기존 웹 개발 프레임워크인 MVC 방식은 모델과 뷰의 양방향 데이터 흐름과 높은 의존성으로 인해 관리와 재활용이 어렵다는 단점이 있다.

Component는 어플리케이션 또는 프로그램을 이루는 독립적인 최소한의 단위(모듈)이다.
컴포넌트 구조는 독립적인 컴포넌트를 조립하여 구성하므로 재사용성이 높고, 단방향 데이터 흐름으로 인해 의존성을 낮추었다.

컴포넌트는 데이터를 props로 입력받고, 컴포넌트 내부에서 데이터 처리를 통해, 저장한 자신의 state(상태)를, DOM Node로 렌더링한다.

function MyComponent({...props}) {
	// props 가공하기...
	
  	// 가공된 data를 기반으로 상태 초기화
    this.state = {}
    
    this.setState = (nextState) => {
      	// state update
     	this.state = {...nextState}
      
      	// updated state를 기반으로 새로 렌더링
      	this.render();
    }
    
    this.render = () => {
     // state를 기반으로 렌더링 
    }
}

CoreComponent - 확장, 재사용

컴포넌트를 하나 하나 작성해가면서, 몇가지 불편한 점을 느끼고 이를 코딩 컨벤션이 아닌, 구조적으로 고쳐보고자 시도하였다. 베이스가 되는 CoreComponent를 작성하고, 이 컴포넌트를 상속시키는 구조를 생각하였다. (이 과정에서 황준일 개발자님의 블로그를 참고하였다.)
1. 새로운 컴포넌트를 작성할 때 마다 모든 메서드를 다시 작성해 줄 필요 없음
2. 어떤 컴포넌트는 생성하는 것만으로 자동으로 마운트, 상위 컴포넌트에서 해당 컴포넌트의 마운트 또는 렌더 메소드를 호출, 어떤 컴포넌트는 라우트 메서드를 통해 호출...코드의 일관성을 향상시키고자 한다.
3. 컴포넌트의 생명 주기 (생성, 마운트, 렌더) 일치시키기!

다음으로는 CoreComponent의 내부 구조이다. 앞서 컴포넌트가 무엇인지에 대해서 알아보았지만, 컴포넌트가 호출되어 생성되고, props를 받아 처리하여 상태를 초기화하고, 이 상태를 기반으로 렌더링하기까지의 과정에 대해서 더 알아보고자 하였다. 이에 리액트 공식문서리액트 라이프 사이클에 관한 문서를 참고하였다.

리액트 컴포넌트는 우선 생성자 함수를 호출하여 컴포넌트 객체를 초기화한다. 이 과정에서 props에 대한 처리, state의 초기화 과정을 마무리하고 본격적인 렌더링 과정으로 진입한다. 대략적으로 보면
생성(constructor) -> 초기화(init) -> 마운트(mount) -> 렌더링(render) -> setEvent 의 과정으로 마운트된다고 보여진다. 상태가 업데이트될 경우 setState -> render 의 순서로, 새로운 상태에 기반하여 렌더링을 수행한다. (setState의 결과로 상태가 변하기 때문에 항시 상태의 변화에 따라 렌더링이 수반된다.)

  • Code
function CoreComponent($target) {
  // constructor - 
  // called before mounted
  this.$container = document.createElement("div");
  $target.appendChild(this.$container);

  this.state = {};

  this.setState = (nextState) => {
    this.state = { ...nextState };

    this.render();
  };

  this.mount = () => {
    this.render();
    this.setEvent();
    // this.componentDidMount();
  };

  this.render = () => {
    this.$container.innerHTML = `
        <div>Core Component</div>
      `;
  };

  this.setEvent = () => {};

  // this.componentDidMount = async () => {};

  // this.componentDidUpdate = async (prevProps, prevState) => {};

  // this.componentWillUnmount = async () => {
  //   await this.$container.remove();
  //   delete this;
  // };

  this.init = () => {
    this.mount();
  };
}

CoreComponent.js 작성기

function vs. class

황준일 개발자님 블로그를 보면 class를 사용하여 컴포넌트를 작성하였다. 사실 많은 VanillaJS를 사용한 웹 컴포넌트 개발 블로그를 보면 class를 사용한 경우가 많았다. class가 제공하는 확장extendconstructor, super의 기능이, 독립적이면서도 최소한의 단위이고, 재사용성과 확장성을 지향하는 컴포넌트 구조와 잘 맞는다고 생각하였다.

그러나 개발을 배워가는 단계인 입장에서, class는 정말 강력하고 좋은 도구이지만, 그 원리를 가려버릴 수 있겠다고 생각했다. 우선 function, scope, context, this 등 JS의 문법적 특성에 대한 이해를 높여야겠다고 생각해서 class를 사용하지 않고 확장성 있는 함수를 만들어보고자 하였다.

구조

const $app = document.querySelector('.app');
new CoreComponent($app);

extends - 어떻게...상속시키지..?

function CoreComponent($target) {
	...
    this.render = () => {
     	this.$container.innerHTML = `CoreComponent`;
    }
    ...
}

function extendsCoreComponent($target, thisOfComponent) {
  const NewComponent = CoreComponent.bind(thisOfComponent);
  
  NewComponent($target);
}

function TestComponent($target, {...props} = {}) {
 	extendsCoreComponent($target, this);
  
  	this.render = () => {
     	this.$container `
			<div>Test</div>
		` 
    }
}
  
new TestComponent($app, {...props}).init();

처음엔 for (const key in new CoreComponent($target)) 과 같은 방법을 사용해서, 새로운 컴포넌트가 CoreComponent의 메서드를 가질 수 있도록 하였다. 그러나 이 경우 클로저 문제가 발생하였다. 상속 받은 컴포넌트가 사용하는 메서드가, 상속 받은 지점에서 새로 선언하지 않는 이상 결국 자신이 선언되었던, new CoreComponent의 환경에서 선언되었던 대로 실행이 되는 것이다. 이에 대해서 며칠을 고민하다 bind를 사용하여 문제를 해결하였다.

  • thisnew
    new ConstructorFunction()을 통해 객체를 생성할 때 그 과정에 대해 알아보자.
    new 연산자를 통해 객체를 생성하면, 이 때 우선 this 객체가 생성된다.
    그 다음으로 ConstructorFunctio은 내부의 코드를 실행시킨다. 이 과정에서 변수는 일종의 private한, 객체 내부에서만 접근 가능한 변수로 작동한다. 그리고 프로퍼티/메서드는 외부에서 호출할 수 있는 변수/함수의 역할을 하게 된다.
    이 프로퍼티/메서드는 생성된 this 객체에 등록된다.
    마지막으로 this 객체를 반환한다.
  • new TestComponent
    new로 호출되었으므로 this객체가 생성되었다. 직후 초기화 과정에서 extendCoreComponent함수를 호출하고, 이 과정에서 TestComponentthis객체를 인자로 전달한다.
  • extndCoreComponent
    TestComponent 객체의 thisthisOfComponent 인자로 전달받는다.
    새로운 함수를 선언하는데, 이 함수는 CoreComponent 생성자 함수이지만, 이 때 thisthisOfComponentbind한 함수이다. 그러므로 NewComponent 함수를 호출하면, CoreComponent의 코드가 실행되고, 이 때 프로퍼티/메서드는 CoreComponent의 객체가 아닌, 전달받은 thisOfComponent 객체에 등록된다.

결과적으로 extendCoreComponent를 호출한 TestComponentCoreComponent의 메서드를 자신의 메서드로 가지고 있다. 그리고 이 메서드에 다시 함수/변수를 할당하여 사용할 수 있다.
CoreComponent를 상속하는 컴포넌트들은 가급적이면 init, render, setEvent만을 새롭게 작성한다. 이를 통해, 서로 다른 컴포넌트들이 독립적인, 고유한 'props 인수, 가공 및 초기화 과정', '렌더링', '이벤트 등록'을 갖지만 그 순서는 CoreComponent에서 선언한 setState, mount의 순서를 따르도록 통일 시켰다

profile
가보자구

0개의 댓글