React 1 Depth Inside

라코마코·2021년 9월 20일
0

React

목록 보기
1/1
post-thumbnail

🤗 안녕하세요~ 라코마코입니다.

오늘 포스팅에서는 React 1 Depth Inside 주제로 포스팅해볼 예정입니다.

네 정말 1Depth 정도만 들어가보며 수박 겉 핥기 식으로 리액트에 대해서 공부할 예정입니다.

저는 Vue.js의 가상돔 라이브러리인 Snabbdom을 이용해서 React와 비슷한 프론트엔드 프레임워크를 만들어본 경험이 있는데요.

프로젝트를 진행하면서 배웠던것들 구체적으로 JSX, hook과 closure의 관계에 대해서 포스팅에서 다뤄볼 예정입니다.

JSX

리액트를 처음 하는 리액트 초보자들은 가끔 이런 문구때문에 혼란해하신 경험이 있을것입니다.

" React Component는 JS 그 자체다. "

import React from 'react';
import SomeComponent from './some-component';

const Template = () => {
  return <div>
    <div>This is Div ya~!</div>
    <SomeComponent/>
    </div> 
}
// 응 아닌데? HTML 코드 있는데?

위는 전형적인 React 코드입니다.

보시다시피 함수 return 구문에는 html (JSX)가 있죠. 겉으로 보기엔 Component는 JS + HTML 같습니다.

이 부분을 이해하기 위해서는 BabelReact.createElement()에 대해서 짚고 넘어가야합니다.

JSX 1 depth inside

우리 포스팅은 Babel이 주목적이 아니니 Babel에 대해선 한줄로 짧게 설명 하고 넘어가겠습니다. ( 이부분은 Babel을 전문적으로 다룬 포스팅을 따로 보시는걸 추천드립니다. )

한줄로 말하면 " JS코드를 JS코드로 트랜스파일링 하는것 " 입니다.

그렇다면 React.createElement()은 무엇일까요?

바로 React의 가상돔 Element를 만드는 Helper 함수입니다.

React.createElement(component, props, ...children)

각 매개변수로 가상돔 Element을 만들 정보를 받아 만들어내는 코드입니다.

( 가상돔에 대해서 잘 모르시다면 따로 전문적으로 다루는 포스팅을 보시는걸 추천드립니다. )

그래서 BabelReact.createElement가 무슨 상관이냐구요? React에서 Babel은 JSX를 만나면 React.createElement로 트랜스파일링 합니다!

바로 이렇게요!

/*
const Template = () => {
  return <div>
    <div>This is Div ya~!</div>
    <SomeComponent/>
    </div>
}
이 코드가 대략 이렇게 변합니다.
*/

const Template = () => {
  return React.createElement("div", null,
     React.createElement("div", null, "This is Div ya~!"),
     React.createElement(SomeComponent, null)
  );
};

잠깐! 저희는 Babel에게 이렇게 변환하라고 지시하지 않았는데 어떻게 알고 이렇게 변경한걸까요?

우리가 설정한 React Babel Preset에 이런 코드가 있기 때문입니다.

  plugins: [
    [
      "@babel/plugin-transform-react-jsx",
      {
        pragma: "React.createElement", // jsx를 만나면 React.createElement로 변경하라!
        throwIfNamespace: false,
      },
    ],
    "@babel/plugin-transform-classes",
    "@babel/plugin-transform-arrow-functions",
    "@babel/plugin-transform-shorthand-properties",
  ],

우리가 (구버전 기준) React Component상단에 항상 React를 선언한 이유도 Babel과 관련있습니다.

// import React from 'react'; 만약 React가 선언되지 않는다면?
// React is not defined 에러가 발생!

const Template = () => {
  return React.createElement("div", null,
     React.createElement("div", null, "This is Div ya~!"),
     React.createElement(SomeComponent, null) //undefined.createElement()로 연산해버린다!
  );
};

Babel은 js 소스코드를 js로 트랜스파일링 한다는것 기억 나시나요? Babel은 성공적으로 JSX를 React.createElement로 트랜스파일링 했습니다. (그것만 변경시켰습니다.)

하지만 코드를 실행할 시점에는 React가 선언되지 않았고 React는 undefined가되고 undefined의 메소드를 호출하는격이 되기 때문에 에러가 발생하게 됩니다.

정리

" React Component는 JS 그 자체다. "

그 이유는 Babel이 JSX를 React.createElement 함수로 변경하기 때문에 build된 파일에는 js만 남기 때문입니다.

또 우리는 이 파트를 통해서 Component는 그저 컴포넌트를 만들어내는 함수이고, <Component/>는 Babel에 의해서 변형된 함수 라는것을 알 수 있습니다.

Hooks

주의 여기서 부터는 실제 React의 동작과는 다를수가 있습니다. 하지만 큰 맥락에서는 비슷합니다.

우리는 위 파트를 통해서 Component는 그저 함수이고, <Component/>는 Babel에 의해서 변형된 함수임을 알게되었습니다.

React에서 상태가 변경되고 새로운 가상돔을 만들기 위해서는 Component 함수를 다시 실행시켜야 합니다.

const Template = () => {
  const [count,setCount] = useState(0);
  return <div> stateTest {count} </div>
}

// 렌더링 될때마다 Template()가 호출된다.

위 같은 컴포넌트 함수가 리렌더링 될 때 마다 재호출되는거죠.

그렇다면 재호출되면서 useState도 내부적으로 다시 호출되는데 어떻게 상태는 유지될 수 있을까요? 🤔

그 비결은 Closure에 있습니다.

Hooks 1 depth inside

우선 각 컴포넌트마자 자신의 상태를 관리하는 이런 객체가 있다고 가정을 하겠습니다.

{
    // 현재 몇번째 hook이 실행되는지 체크하는 변수 
    //Component 함수가 다시 실행될때는 0으로 초기화됨
    this.cursor = 0,
      
    // 상태 저장 배열
    this.hooks = []
}

cursor를 통해서 현재 몇번째 hook이 실행되는지 체크하고, hooks에는 컴포넌트의 상태, effect에서 실행할 함수들이 저장되어있습니다.

이제 Hook 함수를 보겠습니다. useState만 보겠습니다.

useState: (initVal) => {
  const cursor = this.model.cursor;
  const hooks = this.model.hooks;

  hooks[cursor] = hooks[cursor] || [
    initVal,
    (val) => {
      if (hooks[cursor][0] != val) {
        hooks[cursor][0] = val;
        update(); // v-dom rerender
      }
    },
  ];

  this.model.cursor++;
  return [...hooks[cursor]];
}

생각보다 엄청 짧습니다. 우리가 주목할 부분은 이곳 입니다.

hooks[cursor] = hooks[cursor] || [
  initVal,
  (val) => {
    if (hooks[cursor][0] != val) {
      hooks[cursor][0] = val;
      update(); // v-dom rerender
    }
  },
];

꽤 간단합니다.
코드를 하나씩 살펴봅시다.
이 useState가 호출될 시점에 이미 hooks안에 상태가 있다면?

hooks[cursor] = hooks[cursor] || ...

해당 상태를 리턴하고 useState 함수는 종료됩니다.

하지만 상태가 없고 새로운 상태를 만드는 케이스라면 React의 useState처럼 배열의 첫번째에는 init 값을 넣고 2번째 변수에는 상태 변경 함수를 넣는데 이때 closure가 사용됩니다.

(val) => {
  if (hooks[cursor][0] != val) {
    hooks[cursor][0] = val;
    update(); // v-dom rerender
  }
},

Closure로 현재 cursor를 저장한 후 상태 변경 함수가 실행될 때 마다 해당 hook의 상태값을 꺼내 비교하여 변경, 리렌더링 하는 로직으로 동작합니다.

심플하죠?

const Template = () => {
  const [count,setCount] = useState(0); // cursor : 0
  const [count2,setCount2] = useState(1); // cursor : 1
  console.log('hihi');
  const [count3,setCount3] = useState(1); // cursor : 2
  const [count4,setCount4] = useState(1); // cursor : 3
  const [count5,setCount5] = useState(1); // cursor : 4
  
  setTimeout(()=>{
    setCount3(999); // cursor 2의 상태 변경
  },100);
  return <div> stateTest {count} </div>
}

리액트의 상태는 이게 전부입니다. closure로 상태를 관리한다. 정말 심플한 아이디어의 구현체입니다.

그리고 우리는 여기서 React Hooks Rule이 왜 나왔는지도 알 수 있습니다.

리액트가 정상적으로 작동하기 위해서는 hook들의 cursor도 항상 일치해야합니다.

만약 반복문, 조건문으로 인해서 hook이 더 호출되거나, 호출되지 않아 cursor가 꼬이면 상태를 관리할 수 없기 때문에 이런 규칙이 추가된것이죠.

마치며

" 개구리를 해부만 하지 말고, 개구리를 만들어라 "

이는 개구리를 해부하며 구조를 공부하는것보다 비슷하게나마 개구리를 만들어 보며 왜 이런 구조를 가질 수 밖에 없는지 학습하라는 의미가 담겨져있는 말입니다.

React을 학습하기 위해서 (지금은 Angular를 사용하지만 😂) React와 유사한 프론트엔드 프레임워크를 만들고 거기서 배웠던 것들에 대해서 공유하고자 포스팅 하였습니다.

포스팅을 조금 쉽게 하기위해서 왜곡시키고, 설명을 단축시킨 부분도 있습니다. 이 부분은 넓게 양해를 부탁드립니다. 🙏

0개의 댓글