React | 리액트 한달살기 - 1

Lumpen·2024년 12월 10일
0

React

목록 보기
19/26

vite 환경 구성

vite 빠르다

설치

npm create vite@latest .

개발 시 네이티브 ES Module 을 넘어 HMR(Hot Module Replacement) 같은 다양한 기능을 제공한다

번들링 시 Rollup 기반의 다양한 빌드 커맨드 지원으로
최적화된 정적 리소스를 배포할 수 있게 하고
미리 정의된 설정들을 제공한다

eslint, prettier

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
}

pulgins

  • react: JSX를 포함한 다양한 규칙
  • react-hooks: React Hooks 의 사용 규칙과 의존성 배열 검사
  • a11y: JSX 내에서 웹 접근성 문제를 확인해주는 규칙
  • import: import/export 문법의 유효성 검사 및 일관성 유지

rollup

rollup 플러그인은 하나 이상의 속성, 빌드 훅, 출력 생성 훅을 갖춘 객체로
플러그인은 플러그인별 옵션을 인수로 받아 객체를 반환하는 함수를 내보내는 패키지로 배포해야 한다

vite 의 플러그인은 rollup 번들러를 사용한다
rollup 의 플러그인 인터페이스에 vite 의 특정 옵션을 추가한 형태로 구현되어 있다
vite 플러그인을 한 번 작성하면 개발과 빌드 시 모두 사용이 가능하다

플러그인을 사용하면 번들링 전에 코드를 트랜스파일하거나, node_modules 폴더에서 서드파티 모듈을 찾는 등의 동작을 지정할 수 있다

babel

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'],
      },
    }),
  ],
})

@babel/plugin-transfrom-react-jsx

  • JSX 변환: JSX 코드를 리액트 함수 호출로 변환
  • 생산 코드 생성: 프로덕션 빌드를 위해 최적화된 Javascript 코드를 생성

JSX 문법

Javascript XML 의 약자로 JS 를 확장한 문법으로 리액트 컴포넌트 개발 시 사용되는 문법
JS 코드 내에서 HTML 과 유사한 구문을 작성할 수 있다
HTML 과 JS 코드를 섞어 사용할 수 있기 때문에
처음에는 관심사 분리가 되지 않는다는 이유로 많은 비난을 받았지만
익숙한 코드 스타일로 인해 학습곡선이 낮아
리액트가 인기를 얻는데 한 몫 했다

JSX 의 목적은 단순히 HTML, XML 을 자바스크립트 내부에 표현하는 것은 아니다
다양한 속성을 가진 트리 구조를 토큰화해 자바스크립트로 변환하는데 초점을 두고 있다
자바스크립트 내에서 표현하기 까다로운 XML 스타일의 트리 구문을 작성하기 편리하도록 만든문법

JSX -> JS 실습

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

  • @babel/standalone: 브라우저 환경에서 바벨을 사용 가능하게 한다
  • @babel/plugin-transform-react-jsx: JSX 코드를 리액트 함수 호출로 변환,
    프로덕션 빌드를 위해 최적화된 Javascript 코드를 생성

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',
            },
          ],
        ], 
      },
    }),
  ],
})

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

main.js

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");

createElement

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 가 생성되면서 트리 구조를 만드는 것을 볼 수 있다

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

virtual DOM

가상 DOM 은 SPA 를 구성할 때 기존의 DOM 트리를 기억하고,
변경점을 수집하여 실제 DOM 에 반영할 정보를 모아 변경점에 대한 렌더링을 요청한다
리액트에서는 가상 DOM 을 사용한 SPA 라이브러리
MPA 의 단점인 매 변경점마다 전체 페이지에 대한 요청, 그로 인한 화면 깜빡임을 없애
사용자에게 더 좋은 경험을 준다
가상 DOM 을 계산하고 저장하는 비용을 들여,
전체 페이지 요청을 줄이고 사용자 경험을 개선하기 위해 사용

createElement 는 가상 돔을 생성하는 리액트의 함수

createTree

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

createTree - children 에 형제가 있을 경우

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 를 사용하는 방법으로 다시 변경해야 했다

babel setting

vite.config.js

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 기능 변환:

@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,
}
  • 자동 최적화:

코드 크기와 성능을 최적화하기 위해 불필요한 변환을 피하고 필요한 변환만 적용합니다. 이를 통해 결과 번들 파일의 크기를 줄일 수 있습니다.

@babel/plugin-transform-react-jsx

  • JSX 변환:

JSX 구문을 React의 React.createElement 호출로 변환합니다. 이 변환을 통해 브라우저가 JSX 구문을 이해하지 못하더라도 JavaScript 코드로 변환되어 실행될 수 있습니다.

  • 자동 실행 환경 설정 (automatic):
    runtime: 'automatic',

React 17부터 도입된 자동 실행 환경 설정을 지원합니다. 이를 통해 JSX 파일에서 import React from 'react'를 명시적으로 작성할 필요가 없어집니다.

runtime: 'automatic' 옵션을 사용하면 Babel이 자동으로 필요한 임포트를 추가합니다.

  • 커스텀 JSX 프래그먼트 (JSX Fragment):

React의 Fragment를 사용할 때 <React.Fragment> 대신 빈 태그 <>...</>를 사용할 수 있도록 변환합니다.

path

importSource: '@/src/utils/jsx'
importSource 사용 시 상대 경로를 사용하니 dynamic import 오류가 발생하여
path 설정을 해줘야 했다
path 라이브러리 설치 후 설정

 resolve: {
    alias: {
      '@': path.resolve(__dirname, '.'),
    },
  },

esmoduls

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 형식으로 변환합니다.

esnext, optimizeDeps

build: { target: 'esnext', 
  // 최신 ECMAScript 표준으로 타겟팅 
}, optimizeDeps: { 
  include: 	['@babel/preset-env'], 
    // 종속성 최적화에 포함할 패키지 
},

babel.config.json

{
  "presets": ["@babel/preset-env"],
  "plugins": [
    [
      "@babel/plugin-transform-react-jsx",
      {
        "runtime": "automatic",
        "importSource": "@/src/utils/jsx"
      }
    ]
  ]
}

utils/jsx

createElement.js

const createElement = (type, props, children) => {
  return {
    type,
    props: {
      ...props,
      children,
    },
    // key,
    // ref,
  }
}
export default createElement

jsx-runtime.js

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)
}

jsx-runtime 이란

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>;
  • 이전 변환 방식 (React 17 이전):
const element = React.createElement("div", { className: "container" }, "Hello, world!");
  • 새로운 변환 방식 (React 17 및 jsx-runtime 사용 시):
import { jsx as _jsx } from "react/jsx-runtime";

const element = _jsx("div", { className: "container", children: "Hello, world!" });
profile
떠돌이 생활을 하는. 실업자, 부랑 생활을 하는

0개의 댓글