[프레임워크 제작]환경 구성 연대기(최종)

MIlo·2025년 6월 16일
  • 사실 부끄럽지만 그동안 코드를 짜면서 config 파일은 전혀 건드리지 않고 (vite가 만들어주는거 그대로 사용하는...) 코딩을 해왔어서 꼭 해야 할 것 같다고 생각했다.
  • 내가 만든 프레임워크랑 프레임워크를 테스트하는 환경을 분리했기에, 이에 맞는 환경설정을 내가 직접 해야만 했다.

🎯 초기 목표

  1. 프레임워크 코드 (/core)

    • 순수 TypeScript(.ts)로만 작성
    • TS 컴파일러는 타입 검사만 수행 (JSX 변환 불필요)
  2. 데모/테스트 코드 (/jsx-demo/*.jsx)

    • Vite(ESBuild)로만 .jsx → .js 트랜스파일
    • Classic 런타임 방식 적용
      • 팩토리 함수: MyReact.createElement
      • 프래그먼트: MyReact.Fragment
      • 자동 import 주입: jsxInject
  3. 별칭(alias)

    • @core/*, @/* 등이 VSCode와 Vite 양쪽에서 정상 작동
  4. 개발 서버(root)

    • 웹 루트는 /jsx-demo
    • /src (프레임워크 코드)와 /jsx-demo 모두 파일 시스템에서 서빙 허용

🔧 초기 설정

1) tsconfig.json

{
  "compilerOptions": {
    "allowJs": true,
    "jsx": "preserve",        // JSX를 보존만 – 변환은 ESBuild에 위임
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "baseUrl": "src",
    "paths": {
      "@/*":       ["*"],
      "@shared/*": ["shared/*"],
      "@core/*":   ["core/*"]  // bare "@core" 매핑 누락
    },
    "moduleResolution": "bundler",
    "noEmit": true,
    "strict": true,
    "types": ["vitest"]
  },
  "include": ["src", "jsx-demo"]
}

2) vite.config.ts


import { defineConfig } from "vite";
import path from "path";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  // ▶ 개발 서버 루트: 데모 폴더만 호스팅
  root: path.resolve(__dirname, "jsx-demo"),

  plugins: [
    // ▶ 최상위 tsconfig.json 경로 직접 지정
    tsconfigPaths({
      projects: [path.resolve(__dirname, "tsconfig.json")]
    }),
    /*
    // Babel 기반 React 플러그인 대신 ESBuild 사용
    react({
      jsxRuntime: "classic",
    }),
    */
  ],

  esbuild: {
    // ▶ Classic JSX 런타임 지정
    jsxFactory:  "MyReact.createElement",
    jsxFragment: "MyReact.Fragment",
    jsxInject:  `import MyReact from '@/core';`,
    // ▶ include 패턴 누락으로 초기 .jsx 처리 실패
    // include: [/\.jsx$/],
  },

  server: {
    fs: {
      // ▶ /src + /jsx-demo 모두 서빙 허용
      allow: [
        path.resolve(__dirname, "src"),
        path.resolve(__dirname, "jsx-demo"),
      ],
    },
    port: 3000,
    open: true,
  },

  build: {
    outDir: "jsx-demo/dist",
    sourcemap: true,
  },
});

⸻

🐞 주요 이슈 & 해결

미리보기

✅ TSConfig 별칭(alias) 미인식

  • 증상 src/core/index.ts 같은 프레임워크 코드에서는 import createElement from "@core/createElement"가 동작하지만, 테스트용 jsx-demo 폴더에서는 별칭이 인식되지 않음.
  • 원인
    • Vite 설정의 root가 jsx-demo여서, vite-tsconfig-paths 플러그인이 jsx-demo/tsconfig.json만 찾도록 동작했기 때문.
    • 실제 프레임 워크의 코드는 root 경로가 아닌 다른 경로에 있어서 따로 경로를 명시해줘야한다.
  • 해결 vite.config.ts에서 플러그인 호출부를 이렇게 수정:
// vite.config.ts
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  root: path.resolve(__dirname, "jsx-demo"),
  plugins: [
    tsconfigPaths({
      // 프로젝트 최상위의 tsconfig.json을 직접 지정
      projects: [ path.resolve(__dirname, "tsconfig.json") ]
    }),
    // (plugin-react는 잠시 주석 처리)
  ],
  // …그 외 설정…
});
  • vite-tsconfig-paths가 올바른 paths 설정을 읽어와, 데모 환경에서도 별칭이 적용됨.


✅ Vite root 와 스크립트 로딩 경로

  • 설정
root: path.resolve(__dirname, "jsx-demo"),
  • → 개발 서버의 웹 루트를 프로젝트 내 jsx-demo/ 폴더로 지정.(프레임 워크 테스트 전용 폴더)
  • index.html 수정
<!-- bundle.js 대신 ES 모듈 직통 로드 -->
<script type="module" src="app.jsx"></script>
  • → dev 모드에서는 빌드된 dist가 아니라, Vite가 메모리에서 제공하는 app.jsx를 직접 불러와야 함.

✅ 404 & FS-Allow 에러

  1. 404 에러
GET /dist/bundle.js 404
  1. → dev 모드엔 bundle.js가 없어서 404 발생.
  2. FS-Allow 에러
… outside of Vite serving allow list
  1. → Vite는 기본적으로 src와 node_modules/vite/dist/client만 서빙 허용.
  • 해결
server: {
  fs: {
    allow: [
      path.resolve(__dirname, "src"),         // 기존 프레임워크 코드
      path.resolve(__dirname, "jsx-demo")     // 테스트 데모 코드
    ]
  }
}

✅ esbuild include 패턴 문제

  • 증상
Pre-transform error: Failed to load url /app.jsx
  • → esbuild 변환 대상이 아니라고 인식됨.
  • 원인 esbuild.include가 /jsx-demo\/.*.jsx$/로만 정의돼, /app.jsx 경로와 매칭되지 않음.
  • 해결
esbuild: {
  loader: "jsx",
  include: [/\.jsx$/],       // .jsx로 끝나는 모든 파일을 처리
  jsxFactory: "MyReact.createElement",
  jsxFragment: "MyReact.Fragment",
  jsxInject: `import MyReact from '@core';`,
}

✅ 베어(alias-only) @core 미인식

  • 증상
Failed to resolve import "@core" from "jsx-demo/app.jsx"
  • 원인 tsconfig.json의 paths에 "@core/*"만 있고 "@core" 단독 매핑이 없었음.
  • 해결
// vite.config.json
 esbuild: {
    // 오직 데모 폴더의 .jsx 만 처리
    // include: [/jsx-demo\/.*\.jsx$/],

    // Classic 런타임 프래그마 지정

    jsxFactory: "MyReact.createElement",

    jsxFragment: "MyReact.Fragment",

//별칭 경로 수정
    jsxInject: `import MyReact from '@/core';`,
  },

✅ 중복 선언 오류

  • 증상
Identifier 'MyReact' has already been declared
  • 원인 app.jsx에 수동 import MyReact from '@core';와, esbuild.jsxInject에 의한 자동 주입이 중복.
  • 해결 app.jsx에서 수동 import MyReact 구문 제거. → jsxInject가 빌드 시 자동으로 맨 위에 추가.

✅ TS 파일 파싱 오류

  • 증상
Unexpected token '{' at createElement.ts
  • 원인 esbuild loader: "jsx"가 지정돼 .ts 파일은 전혀 트랜스폼되지 않고, 브라우저가 원본 TypeScript를 만나 문법 오류 발생. exbuild가 기존의 프레임워크 코드인 .ts 코드들을 트랜스폼하지 않았음…
  • 해결 esbuild.loader 옵션 제거(또는 .ts 제외). → Vite 기본 TS 트랜스폼 로더가 .ts/.tsx 파일을 정상 처리.

🤔의문점

-> 근데 vite.config 파일에 root 경로를 테스트 환경인 jsx-demo 를 지정했는데 읽어 올 수 없었을까?

우선 옵션 설명 부터

  • → root 옵션과 server.fs.allow 옵션은 완전히 다른 역할을 수행한다.
  1. root: path.resolve(__dirname, "jsx-demo")

    이건 “Vite가 어느 폴더를 웹 루트(= / URL)로 삼을지”를 결정해 준다.

    • jsx-demo/index.html 이 웹의 엔트리 포인트가 되고,
    • 정적 에셋이나 모듈 경로를 /app.jsx, /src/foo.js 처럼 해석할 때 기준이 되는 폴더임.
  2. server.fs.allow: [ … ]

    이건 “Vite 개발 서버가 파일 시스템 상에서 어느 폴더까지 읽어와도 안전한지”를 화이트리스트로 지정해 주는 보안 설정이다.

    • 기본적으로 Vite는 프로젝트 루트(= root)와 node_modules/vite/dist/client만 허용한다.
    • 하지만 이때 allow를 지정하면 “기본 허용 범위 대신” 이 배열 안의 절대경로만 서빙을 허락한다.
    • 즉 allow: [path.resolve(__dirname, "src")] 만 쓰면, jsx-demo 폴더는 허용되지 않아서 404나 FS-ALLOW 에러가 발생한다..

그래서 allow 리스트에 jsx-demo 경로를 명시적으로 추가해야만,

  • Vite가 jsx-demo/app.jsx 를 “이제 파일 시스템에서 읽어도 된다!”
  • 그러면서 Pre-transform이나 404 에러 없이 정상 제공할 수 있게 된다.

정리하자면:

  • root는 “웹상의 URL → 실제 폴더 매핑 기준”
  • fs.allow는 “파일 시스템 보안상 이 폴더도 읽어와라”

최종 설정 요약

tsconfig.json

{
  "compilerOptions": {
    "baseUrl": "src",
    "paths": {
      "@/*":       ["*"],
      "@shared/*":["shared/*"],
      "@core/*":   ["core/*"],
      "@core":     ["core/index.ts"]
    },
    "jsx": "preserve",
    /* …strict, bundler 모드 옵션… */
  },
  "include": ["src", "jsx-demo"]
}

vite.config.ts

import { defineConfig } from "vite";
import path from "path";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  root: path.resolve(__dirname, "jsx-demo"),
  plugins: [
    tsconfigPaths({ projects: [ path.resolve(__dirname, "tsconfig.json") ] })
  ],
  esbuild: {
    include: [/\.jsx$/],
    jsxFactory: "MyReact.createElement",
    jsxFragment: "MyReact.Fragment",
    jsxInject: `import MyReact from '@core';`,
  },
  server: {
    fs: {
      allow: [
        path.resolve(__dirname, "src"),
        path.resolve(__dirname, "jsx-demo")
      ]
    },
    port: 3000,
    open: true
  },
  build: {
    outDir: "jsx-demo/dist",
    sourcemap: true
  }
});

index.html

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>JSX Demo</title>
</head>
<body>
  <div id="root"></div>
  <script type="module" src="/app.jsx"></script>
</body>
</html>

app.jsx

function App() {
  return (
    <div>
      <h1>JSX Demo</h1>
      <p>이것은 JSX 데모입니다.</p>
    </div>
  );
}

const root = document.getElementById("root");
MyReact.render(<App />, root);

⸻

✅ 최종 개선된 설정

tsconfig.json

{
  "compilerOptions": {
    "baseUrl": "src",
    "paths": {
      "@/*":       ["*"],
      "@shared/*": ["shared/*"],
      "@core/*":   ["core/*"],
      "@core":     ["core/index.ts"]  // bare import 매핑 추가
    },
    "jsx": "preserve",                // JSX 보존
    "moduleResolution": "bundler",
    "strict": true,
    "noEmit": true,
    "types": ["vitest"]
  },
  "include": ["src", "jsx-demo"]
}

vite.config.ts

import { defineConfig } from "vite";
import path from "path";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  root: path.resolve(__dirname, "jsx-demo"),

  plugins: [
    tsconfigPaths({
      projects: [path.resolve(__dirname, "tsconfig.json")]
    }),
  ],

  esbuild: {
    include: [/\.jsx$/],                   // 모든 .jsx 처리
    jsxFactory:  "MyReact.createElement",  // Classic runtime
    jsxFragment: "MyReact.Fragment",
    jsxInject:  `import MyReact from '@core';`
  },

  server: {
    fs: {
      allow: [
        path.resolve(__dirname, "src"),
        path.resolve(__dirname, "jsx-demo")
      ]
    },
    port: 3000,
    open: true,
  },

  build: {
    outDir: "jsx-demo/dist",
    sourcemap: true,
  },
});

⸻

🔑 핵심 정리

  • 프레임워크 코드(/core): tsc --noEmit로 타입 체크만
  • JSX 코드(/jsx-demo): ESBuild Classic 런타임으로 변환
  • Alias: bare @core 포함, VSCode & Vite 모두 인식
  • 서빙 권한: fs.allow로 /src + /jsx-demo 허용
  • 중복 import 제거 & include 패턴 명시로 트랜스폼 누락 방지
profile
앵맹!

0개의 댓글