fe-migrate

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

FE Migration Guide

前端迁移指南

$ARGUMENTS
에서 마이그레이션 소스와 타겟을 파싱하여 단계별 가이드를 제공하고 실행한다.
$ARGUMENTS
中解析迁移源和目标,提供并执行分步指南。

지원 마이그레이션

支持的迁移场景

FromTo키워드
Pages RouterApp Router
pages app-router
JavaScriptTypeScript
js typescript
,
js ts
CRA (Create React App)Vite
cra vite
CSS/SCSSTailwind CSS
css tailwind
Class ComponentsHooks
class hooks
ReduxZustand
redux zustand
JestVitest
jest vitest
AxiosFetch
axios fetch
Moment.jsdate-fns
moment date-fns
인자가 없으면 사용자에게 어떤 마이그레이션을 원하는지 질문한다.
源技术栈目标技术栈关键词
Pages RouterApp Router
pages app-router
JavaScriptTypeScript
js typescript
,
js ts
CRA (Create React App)Vite
cra vite
CSS/SCSSTailwind CSS
css tailwind
Class ComponentsHooks
class hooks
ReduxZustand
redux zustand
JestVitest
jest vitest
AxiosFetch
axios fetch
Moment.jsdate-fns
moment date-fns
若未传入参数,则询问用户需要哪种迁移。

마이그레이션 공통 절차

迁移通用流程

  1. 현재 상태 분석: 프로젝트 구조, 의존성, 설정 파일을 파악한다
  2. 영향 범위 파악: 변경이 필요한 파일 목록을 Glob/Grep으로 추출한다
  3. 마이그레이션 계획 제시: 단계별 작업 목록을 사용자에게 보여준다
  4. 단계별 실행: 승인 후 순차적으로 변환을 진행한다
  5. 검증: 빌드 및 테스트 통과를 확인한다

  1. 当前状态分析:梳理项目结构、依赖项、配置文件
  2. 影响范围确认:通过Glob/Grep提取需要修改的文件列表
  3. 制定迁移计划:向用户展示分步任务清单
  4. 分步执行:获得用户确认后依次完成转换
  5. 验证:确认构建及测试全部通过

Pages Router → App Router

Pages Router → App Router

단계별 가이드

分步指南

1단계: 기반 설정
src/app/layout.tsx     ← pages/_app.tsx 에서 이동
src/app/page.tsx       ← pages/index.tsx 에서 이동
src/app/globals.css    ← styles/globals.css 에서 이동
2단계: 페이지 변환 규칙
Pages RouterApp Router
pages/about.tsx
app/about/page.tsx
pages/blog/[slug].tsx
app/blog/[slug]/page.tsx
pages/api/users.ts
app/api/users/route.ts
pages/_error.tsx
app/error.tsx
pages/404.tsx
app/not-found.tsx
3단계: 데이터 페칭 변환
tsx
// Before: getServerSideProps
export async function getServerSideProps() {
  const data = await fetchData();
  return { props: { data } };
}
export default function Page({ data }) { /* ... */ }

// After: Server Component async
export default async function Page() {
  const data = await fetchData();
  return /* ... */;
}
tsx
// Before: getStaticProps + getStaticPaths
export async function getStaticPaths() {
  return { paths: [...], fallback: false };
}
export async function getStaticProps({ params }) {
  const data = await fetchData(params.id);
  return { props: { data }, revalidate: 60 };
}

// After: generateStaticParams + fetch with revalidate
export async function generateStaticParams() {
  return [...];
}
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const data = await fetchData(id);
  return /* ... */;
}
4단계: Hook/상태 사용 컴포넌트 → "use client"
  • useState
    ,
    useEffect
    ,
    useContext
    , 이벤트 핸들러가 있는 컴포넌트에
    "use client"
    추가
  • 가능한 Client Component를 작게 분리
5단계: Head → Metadata
tsx
// Before: next/head
import Head from "next/head";
<Head><title>Page</title></Head>

// After: Metadata export
export const metadata: Metadata = {
  title: "Page",
};

1阶段:基础配置迁移
src/app/layout.tsx     ← 从pages/_app.tsx迁移而来
src/app/page.tsx       ← 从pages/index.tsx迁移而来
src/app/globals.css    ← 从styles/globals.css迁移而来
2阶段:页面转换规则
Pages RouterApp Router
pages/about.tsx
app/about/page.tsx
pages/blog/[slug].tsx
app/blog/[slug]/page.tsx
pages/api/users.ts
app/api/users/route.ts
pages/_error.tsx
app/error.tsx
pages/404.tsx
app/not-found.tsx
3阶段:数据获取逻辑转换
tsx
// 之前:getServerSideProps
export async function getServerSideProps() {
  const data = await fetchData();
  return { props: { data } };
}
export default function Page({ data }) { /* ... */ }

// 之后:Server Component异步写法
export default async function Page() {
  const data = await fetchData();
  return /* ... */;
}
tsx
// 之前:getStaticProps + getStaticPaths
export async function getStaticPaths() {
  return { paths: [...], fallback: false };
}
export async function getStaticProps({ params }) {
  const data = await fetchData(params.id);
  return { props: { data }, revalidate: 60 };
}

// 之后:generateStaticParams + 带revalidate的fetch
export async function generateStaticParams() {
  return [...];
}
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const data = await fetchData(id);
  return /* ... */;
}
4阶段:含Hook/状态的组件 → 添加"use client"
  • 对包含
    useState
    useEffect
    useContext
    或事件处理器的组件添加
    "use client"
    指令
  • 尽量将Client Component拆分为更小的模块
5阶段:Head组件 → Metadata导出
tsx
// 之前:next/head
import Head from "next/head";
<Head><title>Page</title></Head>

// 之后:导出Metadata
export const metadata: Metadata = {
  title: "Page",
};

JavaScript → TypeScript

JavaScript → TypeScript

단계별 가이드

分步指南

1단계: TypeScript 설치 & 설정
bash
pnpm add -D typescript @types/react @types/react-dom @types/node
npx tsc --init
2단계: 파일 확장자 변경
  • .js
    .ts
    (로직 파일)
  • .jsx
    .tsx
    (JSX 포함 파일)
3단계: 점진적 타입 추가
typescript
// tsconfig.json — 느슨하게 시작
{
  "compilerOptions": {
    "strict": false,           // 점진적으로 true로 전환
    "allowJs": true,           // JS 파일 허용
    "noImplicitAny": false     // 나중에 true로
  }
}
4단계: strict mode 순차 활성화
  1. noImplicitAny: true
  2. strictNullChecks: true
  3. strict: true

1阶段:安装TypeScript并配置
bash
pnpm add -D typescript @types/react @types/react-dom @types/node
npx tsc --init
2阶段:修改文件扩展名
  • .js
    .ts
    (逻辑文件)
  • .jsx
    .tsx
    (包含JSX的文件)
3阶段:逐步添加类型定义
typescript
// tsconfig.json — 从宽松模式开始
{
  "compilerOptions": {
    "strict": false,           // 逐步切换为true
    "allowJs": true,           // 允许JS文件
    "noImplicitAny": false     // 后续改为true
  }
}
4阶段:依次启用严格模式
  1. noImplicitAny: true
  2. strictNullChecks: true
  3. strict: true

CRA → Vite

CRA → Vite

변환 포인트

转换要点

CRAVite
react-scripts
vite
+
@vitejs/plugin-react
public/index.html
index.html
(루트)
REACT_APP_*
환경변수
VITE_*
환경변수
process.env.REACT_APP_*
import.meta.env.VITE_*
src/setupTests.ts
vitest.config.ts
+ setup

CRAVite
react-scripts
vite
+
@vitejs/plugin-react
public/index.html
index.html
(根目录)
REACT_APP_*
环境变量
VITE_*
环境变量
process.env.REACT_APP_*
import.meta.env.VITE_*
src/setupTests.ts
vitest.config.ts
+ 配置文件

Jest → Vitest

Jest → Vitest

변환 포인트

转换要点

JestVitest
jest.config.js
vitest.config.ts
jest.fn()
vi.fn()
jest.mock()
vi.mock()
jest.spyOn()
vi.spyOn()
@jest/globals
vitest
jest.useFakeTimers()
vi.useFakeTimers()
moduleNameMapper
resolve.alias
in vite config
typescript
// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "path";

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: "jsdom",
    setupFiles: "./src/test/setup.ts",
    css: true,
  },
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
});

JestVitest
jest.config.js
vitest.config.ts
jest.fn()
vi.fn()
jest.mock()
vi.mock()
jest.spyOn()
vi.spyOn()
@jest/globals
vitest
jest.useFakeTimers()
vi.useFakeTimers()
moduleNameMapper
vite配置中的
resolve.alias
typescript
// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "path";

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: "jsdom",
    setupFiles: "./src/test/setup.ts",
    css: true,
  },
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
});

Redux → Zustand

Redux → Zustand

변환 패턴

转换示例

typescript
// Before: Redux Toolkit
const userSlice = createSlice({
  name: "user",
  initialState: { name: "", email: "" },
  reducers: {
    setUser: (state, action) => { Object.assign(state, action.payload); },
    clearUser: () => ({ name: "", email: "" }),
  },
});

// After: Zustand
interface UserState {
  name: string;
  email: string;
  setUser: (user: { name: string; email: string }) => void;
  clearUser: () => void;
}

const useUserStore = create<UserState>()((set) => ({
  name: "",
  email: "",
  setUser: (user) => set(user),
  clearUser: () => set({ name: "", email: "" }),
}));

typescript
// 之前:Redux Toolkit
const userSlice = createSlice({
  name: "user",
  initialState: { name: "", email: "" },
  reducers: {
    setUser: (state, action) => { Object.assign(state, action.payload); },
    clearUser: () => ({ name: "", email: "" }),
  },
});

// 之后:Zustand
interface UserState {
  name: string;
  email: string;
  setUser: (user: { name: string; email: string }) => void;
  clearUser: () => void;
}

const useUserStore = create<UserState>()((set) => ({
  name: "",
  email: "",
  setUser: (user) => set(user),
  clearUser: () => set({ name: "", email: "" }),
}));

실행 규칙

执行规则

  1. 인자가 없으면 사용자에게 마이그레이션 종류를 질문한다
  2. 마이그레이션 전 현재 프로젝트 상태를 반드시 분석한다
  3. 대규모 변경은 단계별로 나눠서 진행하고, 각 단계 후 빌드/테스트를 확인한다
  4. 기존 코드를 삭제하기 전에 새 코드가 동작하는지 확인한다
  5. package.json
    변경이 필요하면 명령어를 안내하고 사용자 승인 후 실행한다
  6. 지원 목록에 없는 마이그레이션도 요청 시 분석 후 가이드를 제공한다
  1. 若无参数,则询问用户需要的迁移类型
  2. 迁移前必须分析当前项目状态
  3. 大规模变更需拆分阶段执行,每个阶段后确认构建/测试通过
  4. 删除原有代码前,需确认新代码可正常运行
  5. 若需修改
    package.json
    ,需先提供命令并获得用户确认后再执行
  6. 对于支持列表外的迁移请求,也需分析后提供指导方案