학습한 내용을 바탕으로 이야기 하면 JSX는 javascript의 확장 문법으로 표준 문법은 아니다.
jsx는 javascript 파일 안에서 HTML과 유사한 문법으로 작성할 수 있게 만들어 준다.
const TodoInput = () => {
return (
<div className={'sanE'}>
ㅋㅋㅋㅋㅋㅋ
<div>SanE</div>
</div>
);
};
위의 코드를 보면 분명 자바스크립트 코드인데 그 안에 HTML이 들어있는 것처럼 보인다.
JSX를 사용하면 위의 코드에서 느낄 수 있듯 가독성이 좋아진다.
예를 들어 저 모든 테그를 document.createElement를 통해 추가한다고 생각해보자.
지금 보이는 코드와 비교해서 분명히 가독성이 떨어질 것이다.
또한 HTML과 JS 코드를 한 곳에서 다 작성할 수 있기 때문에 생산성 측면에서도 장점을 가지며, JSX를 사용하면 자동으로 모든 값을 렌더링하기 전에 문자열로 변환하기 때문에
XSS 공격 방지에도 좋다.
JSX는 JavaScript로 바뀌고 JSX에서 작성된 어트리뷰트는 JavaScript 객체의 키가 됩니다. 컴포넌트에서는 종종 어트리뷰트를 변수로 읽고 싶은 경우가 있습니다. 그러나 JavaScript는 변수명에 제한이 있습니다. 예를 들면, 변수명에 대시를 포함하거나 class처럼 예약어를 사용할 수 없습니다.
참고 - https://ko.react.dev/learn/writing-markup-with-jsx#jsx-putting-markup-into-javascript
이러한 이유 때문에 class라는 이름은 이미 예약어라서 클래스명을 지정할 때는 className으로 작성한다.
해당 과정은 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' }]
]
}
})
]
});
React에서 createElement는 실제 DOM 요소를 생성하기 전에 가상의 DOM 객체를 만드는 핵심 함수이다.
가상돔은 결국 객체라는 것을 잊지말자.
if (typeof node.type === 'function') {
return createElement(node.type(node.props));
}
if (attr.startsWith('on')) {
const eventType = attr.substring(2).toLowerCase();
eventHandlerMap.set($el, {[eventType]: props[attr]});
}
if (attr === 'style' && typeof value === 'object') {
for (const [styleName, styleValue] of Object.entries(value)) {
$el.style[styleName] = styleValue;
}
}
가상돔은 실제 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.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는 다음과 같은 순서로 동작한다.
history.location로 현위치 초기화.Router와 관련된 너무 많은 컴포넌트들이 있었다.
그 모든 컴포넌트들을 직접 구현하기는 너무 복잡하여 Router와 Route 두개만 이용해서 구현하려고 한다.

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 17버전부터 root 엘리먼트에 이벤트를 위임하는 방식으로 이벤트를 등록한다.
그럼 이제 이벤트 등록은 어느 시점에 일어나는지 알아보면, 바로 가상돔이 생성되는 시점에 root 엘리먼트에 부착된다고 보면된다.
솔직히 이렇게 말로 하는거보다 직접 코드를 보는것이 좋을 것 같다.
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);
*/
};
이 설계를 바탕으로 이벤트 실행시 흐름과 이벤트 등록 흐름에 대해 설명하면 다음과 같다.
addEventListener로 등록.dispatchEvent가 실행됨.dispatchEvent 실행.syntheticEvent : Native Event를 포함한 합성 이벤트.dispatchQueue : 가상돔에 저장되어 있는 이벤트 핸들러들을 저장. (함수들이 들어가며, 이벤트 버블링 순서대로 큐에 저장됨.)event.target부터 부모 요소로 탐색하며 event.type과 일치하는 이벤트가 등록되어 있다면 dispatchQueue에 푸쉬.dispatchQueue에 이벤트 버블링에 따라 실행되야하는 함수가 순서대로 들어가게 된다.dispatchQueue 내부에 있는 함수 실행. (이 때 미리 만들어둔 syntheticEvent를 이용하여 실행.)