React 이해하기

SanE·2024년 12월 9일

Frontend 개발

목록 보기
2/6

👨🏻‍💻학습 내용


💡 JSX

JSX?

학습한 내용을 바탕으로 이야기 하면 JSX는 javascript의 확장 문법으로 표준 문법은 아니다.

jsx는 javascript 파일 안에서 HTML과 유사한 문법으로 작성할 수 있게 만들어 준다.

const TodoInput = () => {
    return (
        <div className={'sanE'}>
            ㅋㅋㅋㅋㅋㅋ
            <div>SanE</div>
        </div>
    );
};

위의 코드를 보면 분명 자바스크립트 코드인데 그 안에 HTML이 들어있는 것처럼 보인다.

장점

JSX를 사용하면 위의 코드에서 느낄 수 있듯 가독성이 좋아진다.

예를 들어 저 모든 테그를 document.createElement를 통해 추가한다고 생각해보자.

지금 보이는 코드와 비교해서 분명히 가독성이 떨어질 것이다.

또한 HTML과 JS 코드를 한 곳에서 다 작성할 수 있기 때문에 생산성 측면에서도 장점을 가지며, JSX를 사용하면 자동으로 모든 값을 렌더링하기 전에 문자열로 변환하기 때문에
XSS 공격 방지에도 좋다.

규칙

  1. 하나의 태그로 감싸기
    • 여러 태그를 내보낼 때는 하나의 부모 태그로 감싸서 내보내야 한다.
    • 기본적으로 JSX는 Javascript 객체를 반환하는데 여러개의 태그로 감싸져 있으면 한꺼번에 여러개의 객체를 반환하라는 의미와 같다. Javascript는 하나의 긱채만 반환 가능하기 때문에 여러개의 객체를 반환하고 싶다면 하나의 부모 태그로 감싸서 하나의 객체로 만들어야 한다.
  2. 모든 태그 닫아주기
  3. 대부분의 케이스를 캐멀 케이스로!

    JSX는 JavaScript로 바뀌고 JSX에서 작성된 어트리뷰트는 JavaScript 객체의 키가 됩니다. 컴포넌트에서는 종종 어트리뷰트를 변수로 읽고 싶은 경우가 있습니다. 그러나 JavaScript는 변수명에 제한이 있습니다. 예를 들면, 변수명에 대시를 포함하거나 class처럼 예약어를 사용할 수 없습니다.

    참고 - https://ko.react.dev/learn/writing-markup-with-jsx#jsx-putting-markup-into-javascript

이러한 이유 때문에 class라는 이름은 이미 예약어라서 클래스명을 지정할 때는 className으로 작성한다.

💡 CreateElement & 가상돔

사전 설정

해당 과정은 Vite-React 프로젝트를 이용했습니다.

jsx를 변환하는데 현재 내 설정 값에서는 다른 추가 설정을 해주지 않았기 때문에 자동으로 React 객체를 반환하고 있었다.
따라서 이부분을 수정하기 위해서는 plugin-transform-react-jsx를 사용하여 importSource 를 수정해서 나만의 커스텀 파일을 연결시켜줘야 했다.

따라서 간단하게 아래와 같이 jsx-runtime.js파일을 작성하고 vite.config.js 파일을 아래와 같이 수정했다.

// jsx-runtime.js
import {createElement} from "../view/utils/createElement.js";

export function jsx(type, props) {
    // 직접 만든 createElement를 호출
    return createElement(type, props);
}

export function jsxs(type, props) {
    return createElement(type, props);
}


// vite.config.js
import { defineConfig } from 'vite';
import babel from 'vite-plugin-babel';

export default defineConfig({
    plugins: [
        babel({
            babelConfig: {
                presets: [
                    ['@babel/preset-env', { modules: false }],
                ],
                plugins: [
                    "@babel/plugin-syntax-dynamic-import",
                    ['@babel/plugin-transform-react-jsx', { runtime: 'automatic', importSource: '/custom-jsx-runtime' }]
                ]
            }
        })
    ]
});

CreateElement란?

React에서 createElement는 실제 DOM 요소를 생성하기 전에 가상의 DOM 객체를 만드는 핵심 함수이다.

가상돔은 결국 객체라는 것을 잊지말자.

  1. 컴포넌트 처리
if (typeof node.type === 'function') {
    return createElement(node.type(node.props));
}
  • 함수형 컴포넌트를 처리할 수 있도록 재귀적으로 구현되어 있다.
  • 컴포넌트가 또 다른 컴포넌트를 반환할 수 있어 컴포넌트의 중첩이 가능하다.
  1. 이벤트 핸들링
if (attr.startsWith('on')) {
    const eventType = attr.substring(2).toLowerCase();
    eventHandlerMap.set($el, {[eventType]: props[attr]});
}
  • React의 이벤트 시스템을 모방하여 구현했다.
  • 'onClick', 'onChange' 등의 이벤트를 별도의 Map에 저장하여 관리한다.
  1. 스타일 처리
if (attr === 'style' && typeof value === 'object') {
    for (const [styleName, styleValue] of Object.entries(value)) {
        $el.style[styleName] = styleValue;
    }
}
  • React와 같이 객체 형태로 스타일을 처리할 수 있다.
  • 카멜케이스로 작성된 스타일 속성을 실제 DOM 스타일로 변환한다.

가상돔의 구현

가상돔은 실제 DOM 조작을 최소화하기 위한 중간 단계의 객체이다. 제공된 createVirtualDom 함수를 보면

export const createVirtualDom = (type, props, ...children) => {
    props.children = typeof props.children === 'string' ? 
        [props.children] : props.children;
    return {type, props, children: children.flat()};
};

createElement & createVirtualDom 코드

// createElement.js
import {eventHandlerMap} from "../store/eventMap.js";

export const createElement = (node) => {
    if (typeof node.type === 'function') {
        return createElement(node.type(node.props));
    }
    const props = node.props || {};
    if (typeof node === 'string' || typeof node === 'number') {
        return document.createTextNode(node);
    }

    const $el = document.createElement(node.type);

    for (const [attr, value] of Object.entries(props)) {
        if (value && attr !== 'children') {
            if (attr.startsWith('on')) {
                const eventType = attr.substring(2).toLowerCase();
                eventHandlerMap.set($el, {[eventType]: props[attr]});
            } else if (attr === 'style' && typeof value === 'object') {
                for (const [styleName, styleValue] of Object.entries(value)) {
                    $el.style[styleName] = styleValue;
                }
            } else {
                $el.setAttribute(attr, value);
            }
        }
    }

    let children;
    if (node.props.children) {
        children = node.props.children.map(v => createElement(v));
        children.forEach(child => $el.appendChild(child));
    }

    return $el;
}

// createVirtualDom.js
export const createVirtualDom = (type, props, ...children) => {
    props.children = typeof props.children === 'string' ? [props.children] : props.children;
    return {type, props, children: children.flat()};
};

💡 Router 학습

학습한 내용을 토대로 정리하면 Router는 다음과 같은 순서로 동작한다.

  1. BrowserRouter에서 node.js의 페키지인 history를 이용해 history 객체를 생성.
  2. Router에서 history를 이용해 history.location로 현위치 초기화.
  3. Link 컴포넌트에 의해 a태그 클릭이나 뒤로가기 앞으로가기를 수행하여 url을 변경할 수 있다.
  4. 위의 액션으로 url이 변경되면 미리 등록한 리스너에 의해 랜더링이 다시 됨.

Router 설계

Router와 관련된 너무 많은 컴포넌트들이 있었다.

그 모든 컴포넌트들을 직접 구현하기는 너무 복잡하여 Router와 Route 두개만 이용해서 구현하려고 한다.

설계 이미지

Router 설계 예시

import { createBrowserHistory } from 'history';

const history = createBrowserHistory();

function Router({ children }) {
  const render = () => {
    document.getElementById('app').innerHTML = '';
    children.forEach((child) => {
      // Route 컴포넌트에 history를 prop으로 전달
      const routeElement = child({ history });
      document.getElementById('app').appendChild(routeElement);
    });
  };

  history.listen(() => {
    render(); // 경로 변경 시 다시 렌더링
  });

  return {
    render,
  };
}

💡 React 이벤트

알아야하는 키워드.

  • 이벤트 위임.
  • Synthetic Event(합성 이벤트).
  • Virtual DOM.

위의 이미지를 보면 확인할 수 있듯, React 17버전부터 root 엘리먼트에 이벤트를 위임하는 방식으로 이벤트를 등록한다.

그럼 이제 이벤트 등록은 어느 시점에 일어나는지 알아보면, 바로 가상돔이 생성되는 시점에 root 엘리먼트에 부착된다고 보면된다.

솔직히 이렇게 말로 하는거보다 직접 코드를 보는것이 좋을 것 같다.

  1. 우선 리액트는 브라우저의 고유 이벤트들을 Native Event로 따로 분류하여 매핑을 하여 정리하고 있다.
  2. 이렇게 매핑된 이벤트들을 미리 다 등록을 해둔다.
  3. getEventListeners(document.querySelector('#root')); 명령을 입력해 root에 등록된 이벤트들을 보면 위의 이미지 같이 나온다.

여기까지 보면 이제 한가지 의문이 든다. 그럼 핸들러는 어디서 가져와서 이벤트를 등록하는 걸까?

정답은 하나의 dispatchEvent라는 이벤트 리스너를 만들고 이 함수를 등록하는 방식으로 진행된다.

아래에 이 내용들을 바탕으로 작성한 간단한 설계 코드이다.

const dispatchEvent = (event) => {
    const syntheticEvent  = {} // 리액트에서는 이런 객체를 이용해 Native Event를 감싼다.

    const dispatchQueue = []; // 함수들이 들어가 있음. (이벤트 버블링 순서로.)
    /*

    while {
     event.target의 부모 요소로 하나씩 탐색. (root까지)
     만약 event.type과 같은 이벤트가 등록 되어 있으면
     이벤트(syntheticEvent)를 dispatchQueue에 push.
    }

    for문을 돌며 dispatchQueue에 들어있는 함수 하나씩 실행.
    for(){

    }
    */
};

export const registerEvent = (root) => {
    /*
    리액트에서는 미리 등록된 매핑 객체를 이용해 모든 이밴트를 등록.
    [이벤트, 이벤트, 이벤트]
    각각의 이벤트를 루트에 등록.
    root.addEventListener(이벤트, dispatchEvent);
    */
};

이 설계를 바탕으로 이벤트 실행시 흐름과 이벤트 등록 흐름에 대해 설명하면 다음과 같다.

이벤트 등록 흐름.

  1. Native Event를 객체 혹은 개발자가 원하는 형태로 저장.
  2. 모든 이벤트를 root 엘리먼트에 addEventListener로 등록.

이벤트 발생시 흐름.

  1. click 발생.
  2. root에서 미리 등록해둔 click 이벤트에 의해 dispatchEvent가 실행됨.
  3. dispatchEvent 실행.
    1. syntheticEvent : Native Event를 포함한 합성 이벤트.
    2. dispatchQueue : 가상돔에 저장되어 있는 이벤트 핸들러들을 저장. (함수들이 들어가며, 이벤트 버블링 순서대로 큐에 저장됨.)
    3. event.target부터 부모 요소로 탐색하며 event.type과 일치하는 이벤트가 등록되어 있다면 dispatchQueue에 푸쉬.
    4. 3번 과정을 거치면 dispatchQueue에 이벤트 버블링에 따라 실행되야하는 함수가 순서대로 들어가게 된다.
    5. for문을 돌며 dispatchQueue 내부에 있는 함수 실행. (이 때 미리 만들어둔 syntheticEvent를 이용하여 실행.)

참고 자료

JSX

React createElement

Diffing 알고리즘

babel 설정 변경.

useState

React-Router

History API

React 이벤트.

React 합성 이벤트.

profile
JavaScript를 사용하는 모두를 위해...

0개의 댓글