vite 빠르다
npm create vite@latest .
개발 시 네이티브 ES Module 을 넘어 HMR(Hot Module Replacement) 같은 다양한 기능을 제공한다
번들링 시 Rollup 기반의 다양한 빌드 커맨드 지원으로
최적화된 정적 리소스를 배포할 수 있게 하고
미리 정의된 설정들을 제공한다
eslint.json
{
"extends": [
"plugin:react/recommended",
"airbnb-base",
"plugin:jsx-a11y/recommended",
"plugin:import/errors",
"plugin:import/warnings"
],
"plugins": ["react", "react-hooks", "jsx-a11y", "import"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
.prettierrc
{
"trailingComma": "es5",
"tabWidth": 2,
"printWidth": 120,
"semi": false,
"useTabs": false,
"jsxBracketSameLine": false,
"arrowParens": "avoid",
"bracketSameLine": true,
"bracketSpacing": true,
"singleQuote": true
}
rollup 플러그인은 하나 이상의 속성, 빌드 훅, 출력 생성 훅을 갖춘 객체로
플러그인은 플러그인별 옵션을 인수로 받아 객체를 반환하는 함수를 내보내는 패키지로 배포해야 한다
vite 의 플러그인은 rollup 번들러를 사용한다
rollup 의 플러그인 인터페이스에 vite 의 특정 옵션을 추가한 형태로 구현되어 있다
vite 플러그인을 한 번 작성하면 개발과 빌드 시 모두 사용이 가능하다
플러그인을 사용하면 번들링 전에 코드를 트랜스파일하거나, node_modules 폴더에서 서드파티 모듈을 찾는 등의 동작을 지정할 수 있다
vite 팀은 가능한 더 빠른 경험을 유지하기 위해 기본적으로 babe 을 포함하지 않았다
esbuild 가 babel 로 할 수 있는 많은 작업을 이미 수행할 수 있기 때문
바벨 설정이 필요하다면 꼭 필요한 플러그인만 포함시키는 것이 좋다
.babelrc 설정 파일을 만들지 않고 플러그인에 babelConfig 옵션을 사용하여
설정을 구성할 수 있다
vite plugin 에서 babel 플러그인을 사용하여 바벨 설정을 한다
https://github.com/owlsdepartment/vite-plugin-babel
플러그인을 사용하면 모든 vite 명령 시 바벨을 함께 실행할 수 있다
npm install -D vite-plugin-babel
import { defineConfig } from 'vite'
import babel from 'vite-plugin-babel'
export default defineConfig({
plugins: [
// Babel will try to pick up Babel config files (.babelrc or .babelrc.json)
babel({
babelConfig: {
babelrc: false, // .babelrc 파일을 사용하지 않도록 한다
configFile: false, // babel.config.js 파일을 사용하지 않도록 한다
plugins: ['@babel/plugin-transfrom-react-jsx'],
},
}),
],
})
Javascript XML 의 약자로 JS 를 확장한 문법으로 리액트 컴포넌트 개발 시 사용되는 문법
JS 코드 내에서 HTML 과 유사한 구문을 작성할 수 있다
HTML 과 JS 코드를 섞어 사용할 수 있기 때문에
처음에는 관심사 분리가 되지 않는다는 이유로 많은 비난을 받았지만
익숙한 코드 스타일로 인해 학습곡선이 낮아
리액트가 인기를 얻는데 한 몫 했다
JSX 의 목적은 단순히 HTML, XML 을 자바스크립트 내부에 표현하는 것은 아니다
다양한 속성을 가진 트리 구조를 토큰화해 자바스크립트로 변환하는데 초점을 두고 있다
자바스크립트 내에서 표현하기 까다로운 XML 스타일의 트리 구문을 작성하기 편리하도록 만든문법
JSX 문법은 바벨에 의해 JS 로 트랜스파일링 된다
바벨은 JSX 코드를 React.createElement 함수 호출로 변환한다
const element = <h1 required={true}>Hello, world!</h1>;
const element = React.createElement('h1', {required: true}, 'Hello, world!');
npm install -D @babel/standalone @babel/plugin-transform-react-jsx
vite.config.js
import { defineConfig } from 'vite'
import babel from 'vite-plugin-babel'
export default defineConfig({
plugins: [
babel({
babelConfig: {
babelrc: false,
configFile: false,
plugins: [
[
'@babel/plugin-transform-react-jsx',
{
runtime: 'automatic',
importSource: './src/utils/transform.js',
},
],
],
},
}),
],
})
import * as Babel from '@babel/standalone'
// Babel Standalone 설정 수정
const BABEL_CONFIG = {
plugins: [['transform-react-jsx']],
}
const transform = jsxCode => {
const { code: jsCode } = Babel.transform(jsxCode, BABEL_CONFIG)
return jsCode
}
export default transform
import transform from './transform'
const jsxCode = '<div prop="prop">Transpile</div>'
const jsCode = transform(jsxCode)
console.log(jsCode)
jsx 코드
<div prop="prop">Transpile</div>
js 로 트랜스파일된 코드
/*#__PURE__*/React.createElement("div", { prop: "prop" }, "Transpile");
React.createElement([componentType], [{propsName: props}], [children]);
리액트에서 제공하는 메서드로 JS 코드를 JSX 형식의 React element 로 반환한다
createElement 를 이용해 element 트리를 생성할 수 있다
<div prop="prop">
<span>Transpile</span>
</div>
위 코드를 babel 로 transform 하면
/*#__PURE__*/React.createElement("div", { prop: "prop" }, /*#__PURE__*/React.createElement("span", null, "Transpile")
);
children 에 React.createElement 가 생성되면서 트리 구조를 만드는 것을 볼 수 있다
const createProps = props => {
return Object.entries(props).reduce((acc, [key, value]) => {
acc += ` ${key}="${value}"`
return acc
}, '')
}
const createElement = (type, props, children) => {
return {
type,
props: {
...props,
children,
},
// key,
// ref,
}
}
export default createElement
가상 DOM 은 SPA 를 구성할 때 기존의 DOM 트리를 기억하고,
변경점을 수집하여 실제 DOM 에 반영할 정보를 모아 변경점에 대한 렌더링을 요청한다
리액트에서는 가상 DOM 을 사용한 SPA 라이브러리
MPA 의 단점인 매 변경점마다 전체 페이지에 대한 요청, 그로 인한 화면 깜빡임을 없애
사용자에게 더 좋은 경험을 준다
가상 DOM 을 계산하고 저장하는 비용을 들여,
전체 페이지 요청을 줄이고 사용자 경험을 개선하기 위해 사용
createElement 는 가상 돔을 생성하는 리액트의 함수
import createElement from './createElement'
const parser = new DOMParser()
const regex = /<(\w+)[^<>]*>[\s\S]*<\/\1>/
const checkHasTag = JSXElementString => {
const hasTag = regex.test(JSXElementString)
if (!hasTag) return false
return true
}
const getElementProperties = JSXElementString => {
const xml = parser.parseFromString(JSXElementString, 'application/xml')
const rootElement = xml.documentElement
const type = rootElement.tagName
const props = {}
Array.from(rootElement.attributes).forEach(attr => {
props[attr.name] = attr.value
})
const children = rootElement.innerHTML
return { type, props, children }
}
const createTree = JSXElementString => {
if (!checkHasTag(JSXElementString)) return JSXElementString
const { type, props, children } = getElementProperties(JSXElementString)
return createElement(type, props, createTree(children))
}
export default createTree
import createElement from './createElement.js'
const parser = new DOMParser()
const regex = /<(\w+)[^<>]*>[\s\S]*<\/\1>/
const checkHasTag = JSXElementString => {
const hasTag = regex.test(JSXElementString)
if (!hasTag) return false
return true
}
const getElementProperties = JSXElementString => {
const xml = parser.parseFromString(JSXElementString, 'application/xml')
const rootElement = xml.documentElement
const type = rootElement.tagName
const props = {}
Array.from(rootElement.attributes).forEach(attr => {
props[attr.name] = attr.value
})
// 자식 요소들을 배열로 변환
const childNodes = Array.from(rootElement.childNodes)
// 텍스트 노드와 요소 노드를 모두 포함하는 자식들
const children = childNodes
.map(node => {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent.trim()
}
return node.outerHTML
})
.filter(child => child !== '')
return { type, props, children }
}
const createTree = JSXElementString => {
const JSXCode = JSXElementString.trim()
console.log(JSXCode)
if (!checkHasTag(JSXCode)) return JSXCode
const { type, props, children } = getElementProperties(JSXCode)
// 자식이 여러개인 경우 배열로 처리
if (Array.isArray(children)) {
const childrenTrees = children.map(child => createTree(child))
return createElement(type, props, childrenTrees)
}
return createElement(type, props, createTree(children))
}
export default createTree
함수 컴포넌트는 Virtual DOM 을 반환하는 함수다
각각의 함수가 체이닝되어 tree 를 구성한다
jsxString 을 받아 직접 구현한 transform 을 통해
createElement 를 재귀적으로 처리하는 createTree 를 구현하였는데,
여기서 함정은 transform 이 React 와 많이 다르다는 것이다
JSX 문법 그대로를 처리하지 못하고 문자열인 jsxString 을 정규식으로 풀어낸 것 밖에 되지 않기 때문에 특히 함수를 전달하는 부분에서 차이가 있다
babel.transform 에서는 onClick={} 내에 작성된 함수를 함수 객체 그대로 전달하는 반면
내가 작성한 transform 에서는 onClick 을 문자열로만 전달 가능하여 eval 이나 new Function 을 사용하지 않고는 함수 실행이 불가능했다
또한 문자열로 전달되기 때문에 함수 이름만 전달되고, 기존의 참조를 유지하지 못하였다
transform 을 구현하기 위해서는 JSX 문법을 트랜스파일 하는 트랜스파일러를 만들어야 하는데 그 부분은 또 다른 영역의 문제로 보여 @babel/core 를 사용하는 방법으로 다시 변경해야 했다
import path from 'path'
import { defineConfig } from 'vite'
import babel from 'vite-plugin-babel'
export default defineConfig({
plugins: [
babel({
babelConfig: {
presets: [
[
'@babel/preset-env',
{
targets: {
esmodules: true,
},
modules: false, // CommonJS 변환을 비활성화
},
],
],
plugins: [
[
'@babel/plugin-transform-react-jsx',
{
runtime: 'automatic',
importSource: '@/src/utils/jsx',
},
],
],
},
}),
],
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
},
},
// ESM 관련 설정 추가
build: {
target: 'esnext',
},
optimizeDeps: {
include: ['@babel/preset-env'],
},
})
@babel/preset-env는 최신 JavaScript(ES6+) 문법을 더 널리 지원되는 구형 문법으로 변환합니다. 예를 들어, 화살표 함수(arrow function)나 let/const와 같은 최신 구문을 ES5 호환 코드로 변환합니다.
특정 브라우저나 Node.js버전을 지정하여 타겟 환경을 설정할 수 있습니다. 예를 들어, 구형 브라우저를 지원하거나 최신 브라우저에 맞춰 트랜스파일링할 수 있습니다.
예시: targets 옵션을 사용하여 어떤 환경을 지원할지 지정합니다.
{
targets: {
edge: '17',
firefox: '60',
chrome: '67',
safari: '11.1',
},
}
@babel/preset-env는 필요한 폴리필(polyfill)을 자동으로 결정하고, 필요한 경우 폴리필을 추가하여 최신 JavaScript 기능을 사용할 수 있도록 합니다.
예시: useBuiltIns 옵션을 사용하여 필요한 폴리필만 포함시킬 수 있습니다.
{
useBuiltIns: 'usage',
corejs: 3,
}
코드 크기와 성능을 최적화하기 위해 불필요한 변환을 피하고 필요한 변환만 적용합니다. 이를 통해 결과 번들 파일의 크기를 줄일 수 있습니다.
JSX 구문을 React의 React.createElement 호출로 변환합니다. 이 변환을 통해 브라우저가 JSX 구문을 이해하지 못하더라도 JavaScript 코드로 변환되어 실행될 수 있습니다.
runtime: 'automatic',
React 17부터 도입된 자동 실행 환경 설정을 지원합니다. 이를 통해 JSX 파일에서 import React from 'react'를 명시적으로 작성할 필요가 없어집니다.
runtime: 'automatic' 옵션을 사용하면 Babel이 자동으로 필요한 임포트를 추가합니다.
React의 Fragment를 사용할 때 <React.Fragment> 대신 빈 태그 <>...</>를 사용할 수 있도록 변환합니다.
importSource: '@/src/utils/jsx'
importSource 사용 시 상대 경로를 사용하니 dynamic import 오류가 발생하여
path 설정을 해줘야 했다
path 라이브러리 설치 후 설정
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
},
},
ESmodule 설정
{
targets: {
esmodules: true,
},
modules: false, // CommonJS 변환을 비활성화
},
위 설정을 하지 않으면 import 문이 require 로 변환되어 오류가 발생한다
targets: { esmodules: true }
이 설정은 최신 ES 모듈 환경을 대상으로 코드를 트랜스파일링하도록 Babel에 지시합니다. 이를 통해 최신 브라우저와 ES 모듈을 기본적으로 지원하는 환경에서 코드를 실행할 수 있습니다.
modules: false
이 설정은 Babel이 import 및 export 구문을 CommonJS 형식으로 변환하지 않도록 합니다. ES 모듈 구문을 그대로 유지하여 번들러(예: Webpack, Vite)가 처리할 수 있게 합니다. 이 옵션을 설정하지 않으면, Babel은 기본적으로 ES 모듈 구문을 CommonJS 형식으로 변환합니다.
build: { target: 'esnext',
// 최신 ECMAScript 표준으로 타겟팅
}, optimizeDeps: {
include: ['@babel/preset-env'],
// 종속성 최적화에 포함할 패키지
},
{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
"runtime": "automatic",
"importSource": "@/src/utils/jsx"
}
]
]
}
const createElement = (type, props, children) => {
return {
type,
props: {
...props,
children,
},
// key,
// ref,
}
}
export default createElement
import createElement from './createElement'
export const jsx = (type, props) => {
const { children, ...rest } = props
return createElement(type, { ...rest }, children)
}
export const jsxs = (type, props) => {
const { children, ...rest } = props
return createElement(type, { ...rest }, children)
}
React 17부터 도입된 새로운 JSX 변환 방식을 지원하는 모듈입니다. 이전에는 React 코드에서 JSX를 사용하면 Babel이 이를 React.createElement 호출로 변환했습니다. 그러나 jsx-runtime을 사용하면 더 최적화된 방식으로 JSX 코드를 컴파일할 수 있습니다.
자동 주입: React 17부터는 JSX 변환 시 React를 자동으로 임포트하는 기능이 제공됩니다. 이는 JSX 파일에서 import React from 'react'를 명시적으로 작성할 필요가 없다는 의미입니다.
최적화된 변환: 새로운 방식은 React.createElement 대신 jsx 및 jsxs 함수를 사용하여 더 최적화된 변환을 수행합니다.
디버깅 개선: 더 나은 디버깅 경험을 제공하여 컴파일된 코드가 더욱 명확하고 이해하기 쉬워집니다.
예시:
다음은 JSX 코드가 jsx-runtime을 사용하여 변환되는 방식을 보여주는 예시입니다.
JSX 코드:
const element = <div className="container">Hello, world!</div>;
const element = React.createElement("div", { className: "container" }, "Hello, world!");
import { jsx as _jsx } from "react/jsx-runtime";
const element = _jsx("div", { className: "container", children: "Hello, world!" });