CRA 없이 react app 만들기

adultlee·2023년 11월 13일
9

NDD

목록 보기
4/16
post-thumbnail

0. 서문

지난 시간에 우리가 사용할 기술들에 대해서 짧게 나마 논의한 후 결론을 내릴 수 있었습니다.
우리팀(NDD)은 Next가 아닌 SPA 서비스를 만들것이며, 특히 CRA없이 react 서비스를 구성해보려고 합니다.
본 게시글은 babel 과 webpack에 집중하여 작성되었습니다.

1. 왜 cra없이 진행하게 되었는가?

프레임워크는 다양하고 편리한 기능을 제공합니다.
당연히 여러 유능한 개발자들의 개발로 이루어졌기 때문이지만, 여러가지로 위험하다는 생각이 들었습니다.

  1. 프레임워크를 사용하면 편리한 기능들이 어떻게 제공되는지 모릅니다.
  2. 프레임워크를 사용하기 전 "불편함"을 알고 있어야 한다고 생각합니다.

위와 같은 생각들을 통해 저는 점점 프레임워크를 사용하지 말아야 겠다는 생각이 들었습니다.
적어도 불편함 을 모르는 상태에서 프레임워크로 단기간의 편리함을 찾기는 모순된다는 생각이 들었습니다.

무엇보다 우린 프로젝트를 진행하면서 "성장" 에 초점을 두었습니다.
단기간 프로젝트를 완성하는것이 목표가 아닌,
시간이 오래 걸리더라도 의미 있는 학습을 함께 진행하고 싶었습니다.

부캠에서도 지양한 webpack 기반…!

부스트캠프에서 제공한 그룹프로젝트 가이드라인

하지만 부스트캠프 측에서도 Webpack에 대한 셋팅은 추천하지 않았는데요.
그래도 꼭 진행해보고 싶었습니다. 늘 프레임워크를 사용하며, 부족함을 느끼던 부분을 이번 기회에 채우고 싶었습니다.

2. 초기 프로젝트 셋팅에 필요한것

따라서 초기 프로젝트 셋팅에 필요한 것을 리스트업하게 되었습니다.

  1. React 기반 프로젝트 셋팅이 가능해야한다.
  2. TS 문법을 사용가능해야한다.
  3. Emotion의 css-props 문법이 사용가능해야 한다.
  4. eslint 나 prettier 등 코드 포멧팅이 가능해야한다.

3. CRA는 어떻지?

npx create-react-app [프로젝트 명]

다음과 같은 명령어로 아래와 같은 결과물이 생성됩니다.

https://create-react-app.dev/docs/getting-started

my-app
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
└── src
    ├── App.css
    ├── App.js
    ├── App.test.js
    ├── index.css
    ├── index.js
    ├── logo.svg
    ├── serviceWorker.js
    └── setupTests.js

이 코드 어디에도 앞으로 설명할 webpack, babel 등등 번들링과 컴파일을 진행할 셋팅이 보이질 않습니다.

그 이유는 Create React App (CRA)을 사용하여 React 프로젝트를 생성하면, Webpack, Babel, ESLint 등의 설정이 내부적으로 숨겨져 있기 때문입니다. 이런 방식을 "Zero-Configuration" 또는 "No-Config" 방식이라고 합니다. CRA는 이러한 도구들을 미리 구성하여 제공하기 때문에, 사용자는 복잡한 설정 과정 없이도 바로 React 애플리케이션을 개발하기 시작할 수 있습니다.

물론 접근을 완전 못하는것은 아닙니다. npm run eject 와 같은 명령어를 사용하면, 숨겨진 webpack, babel등에 접근할 수 있습니다.

어떻게 react를 사용할 수 있을까?

위에서 간단하게 설명을 드렸었습니다만, react는 여러가지 도구를 통해서 브라우저가 읽을 수 있도록 "변환" 해주는 과정이 필요합니다.
단편적으로 React의 JSX라는 XML과 유사한 문법을 사용합니다. 브라우저는 JSX를 이해하지 못하기 때문에, Babel은 JSX를 브라우저가 이해할 수 있는 일반 JavaScript 함수 호출로 변환합니다.

물론 이는 React 에만 한정되는 이야기는 아닙니다. es6의 모듈 시스템과 같은 최신 문법들에대해서도 브라우저가 이해할 수 있는 코드로 변환하는 과정이 필요합니다.

다음과 같이 요약할 수 있을것 같습니다.

Node.js와 문법 문제

Node.js는 기본적으로 최신 JavaScript 문법의 일부를 지원하지 않습니다. 예를 들어, React에서 많이 사용되는 JSX 문법이나 ES6의 import/export 문법 등이 그렇습니다. 이러한 문법을 Node.js가 이해할 수 있도록 변환해야 하는데, 여기서 Babel이 필요합니다.

Babel

Babel은 최신 JavaScript 문법을 Node.js가 이해할 수 있는 이전 버전의 JavaScript로 변환해주는 도구입니다. 이를 통해 우리는 React 앱에서 최신 문법을 자유롭게 사용할 수 있게 됩니다.

Webpack과 파일 빌드/서비스

React 앱은 여러 JavaScript 파일로 구성되어 있습니다. 이 파일들을 웹 브라우저가 이해할 수 있도록 하나의 파일로 합치는 과정이 필요합니다. 이 과정을 '빌드(build)'라고 합니다. Webpack은 이러한 빌드 과정을 관리하는 도구로, 개발 중에 실시간으로 파일을 합치고 필요한 변환을 수행합니다. 또한, 개발 서버를 제공하여 실시간으로 앱을 테스트하고 확인할 수 있게 해줍니다.

4. babel 셋팅을 해보자

Babel은 자바스크립트 컴파일러로, 주로 최신 버전의 자바스크립트(ES6 이상) 코드를 오래된 자바스크립트 버전으로 변환하는 데 사용됩니다. 이런 변환 과정을 통해, 최신 자바스크립트 문법을 사용하여 작성된 코드를 오래된 브라우저나 환경에서도 실행할 수 있게 합니다. Babel의 주요 특징과 기능은 다음과 같습니다:

코드 변환(Transpilation): Babel은 최신 자바스크립트 문법(예: 화살표 함수, 클래스, 템플릿 문자열 등)을 구 버전의 자바스크립트 코드로 변환합니다. 이는 구형 브라우저나 환경에서도 최신 자바스크립트 기능을 사용할 수 있게 해줍니다.

브라우저 호환성: 다양한 브라우저에서 자바스크립트 코드가 일관되게 작동하도록 도와줍니다. 이를 위해 Babel은 특정 브라우저에서 필요한 특정 변환만을 적용할 수 있는 '프리셋(presets)'과 '플러그인(plugins)'을 제공합니다.

JSX 및 React 지원: Babel은 React 개발에 필수적입니다. React에서 사용되는 JSX 문법은 기본적으로 자바스크립트에 내장되어 있지 않으므로, Babel을 사용하여 JSX를 일반 자바스크립트 코드로 변환해야 합니다.

플러그인 및 확장성: Babel은 다양한 플러그인을 통해 확장 가능합니다. 개발자는 필요에 따라 추가적인 기능을 Babel에 플러그인으로 추가하여 사용할 수 있습니다.

ES6 이상의 기능 지원: Babel은 ES6, ES7 등의 새로운 자바스크립트 버전에서 도입된 기능들을 오래된 자바스크립트 버전으로 변환하여, 이전 버전의 자바스크립트 엔진에서도 해당 기능들을 사용할 수 있도록 지원합니다.

// .babelrc 
{
  "presets": [
    "@babel/preset-env",
    [
      "@babel/preset-react",
      { "runtime": "automatic", "importSource": "@emotion/react" }
    ],
    "@babel/preset-typescript"
  ],
  "plugins": ["@emotion/babel-plugin"],
  "targets": "> 0.5%, not dead"
}

한줄씩 설명을 추가해보겠습니다.
Presets은 특정한 목적이나 환경에 맞춰진 일련의 Babel 플러그인들의 묶음입니다. 각 프리셋은 자주 사용되는 특정 설정이나 환경에 최적화된 Babel 플러그인의 조합을 포함합니다. 여기서 사용되는 presets는 다음과 같습니다.

@babel/preset-env

@babel/preset-env는 개발자가 지정한 대상 환경(브라우저, Node.js 등)에 따라 필요한 JavaScript 변환을 자동으로 적용합니다. 예를 들어, 오래된 브라우저에서 실행되는 코드를 개발한다면, 이 프리셋은 ES6 이상의 JavaScript 문법을 오래된 JavaScript 문법으로 변환하여 호환성을 보장합니다.

"폴리필(polyfill)"을 적용한다는 것은 특정 기능이나 API가 지원되지 않는 브라우저나 환경에서도 그 기능을 사용할 수 있도록 하는 코드 또는 도구를 제공한다는 의미입니다. Babel과 같은 도구를 사용할 때, 폴리필은 자동으로 적용될 수 있습니다. 예를 들어, @babel/preset-env는 필요한 폴리필을 프로젝트의 대상 환경에 맞춰 자동으로 포함시킵니다. 이는 개발자가 별도로 각 브라우저의 호환성을 체크하고 필요한 폴리필을 수동으로 추가하는 수고를 덜어줍니다.

["@babel/preset-react",{ "runtime": "automatic", "importSource": "@emotion/react" }]

  1. @babel/preset-react
    이 프리셋은 JSX와 같은 React의 문법을 일반 JavaScript로 변환합니다. React 컴포넌트에서 사용되는 JSX 문법은 브라우저가 직접 이해할 수 없기 때문에, Babel을 통해 호환 가능한 JavaScript 코드로 변환시켜줍니다.
  2. runtime: "automatic"
    runtime 옵션은 JSX를 어떻게 변환할지를 결정합니다. "automatic"으로 설정하면, Babel은 JSX를 변환할 때 React의 import 문을 자동으로 추가합니다. 이는 React 17 이후부터 도입된 새로운 JSX 변환 방식입니다. 이 방식을 사용하면, 개발자는 각 파일에서 import React from 'react'를 명시적으로 쓰지 않아도됩니다. Babel이 필요한 React 함수를 자동으로 가져오기 때문입니다.
  3. importSource: "@emotion/react" (아주아주아주 중요!!!!!)
    importSource 옵션은 JSX 변환 시 사용할 라이브러리의 출처를 지정합니다. 여기서 "@emotion/react"로 설정되어 있으며, 이는 Emotion 라이브러리를 사용한다는 것을 의미합니다. Emotion은 CSS-in-JS 라이브러리 중 하나로, 이 설정을 통해 Emotion의 JSX 프라그먼트를 사용하여 스타일링된 컴포넌트를 쉽게 작성할 수 있습니다.
    다음의 코드를 통해서 Emotion의 css-props를 사용가능하게 변경이 가능합니다. (물론 tsconfig에서 jsxImport를 동시에 변경시켜야만 합니다.)

"@babel/preset-typescript"

Babel을 사용하여 TypeScript 코드를 일반 JavaScript로 변환하기 위한 Babel 프리셋입니다. TypeScript는 JavaScript에 타입을 추가한 확장 언어로, 더 안정적이고 유지보수하기 쉬운 코드 작성을 가능하게 합니다. 그러나 브라우저나 일반적인 JavaScript 환경에서 직접 실행될 수 없기 때문에, TypeScript 코드를 일반 JavaScript 코드로 변환하는 과정이 필요합니다. 이때 @babel/preset-typescript가 사용됩니다.

"plugins": ["@emotion/babel-plugin"],

emotion에서 제공하는 babel-plugin 입니다. 여러가지 편의 기능을 제공합니다. 필수적이진 않습니다.
이 플러그인은 Emotion 스타일 코드의 성능을 최적화 하는 기능을 수행합니다. 예를 들어, 스타일이 컴파일 타임에 정적으로 결정될 수 있다면, 런타임 오버헤드를 줄이기 위해 이를 미리 계산합니다.

"targets": "> 0.5%, not dead"

해당 설정은 babel이 지원할 수 있는 브라우저의 범위에 대한 조건을 명시합니다.
위의 문맥은 다음과 같은 의미를 가집니다.
"> 0.5%": 전 세계 사용자의 0.5% 이상이 사용하는 브라우저 버전을 대상으로 합니다. 즉, 전체 인터넷 사용자의 상당 부분이 사용하는 브라우저 버전에 대해 코드를 최적화하고 변환합니다.

"not dead": '죽은' 브라우저는 제외합니다. 여기서 '죽은' 브라우저란 공식적인 지원이 종료된 브라우저를 의미합니다. 예를 들어, 보안 업데이트나 기술 지원이 더 이상 제공되지 않는 브라우저는 이 범주에 속합니다.

이 밖에도 Source Maps (sourceMaps),Ignore / Only (ignore, only),Environment (env),Comments (comments),Compact (compact) 등등의 속성이 있지만, 좀 더 프로젝트가 성숙해 진 이후에 필요성이 대두된다면 도입할 예정입니다.

5. webpack 셋팅을 해보자

webpack의 모든 설정을 다루기 보단, babel과 연결되는 부분을 중심으로 나머지 부분들은 상대적으로 얕게 다뤄보겠습니다.

웹 애플리케이션은 수많은 JavaScript 파일, CSS, 이미지 등으로 구성됩니다. Webpack은 이러한 다양한 자원들을 모아서 브라우저가 이해할 수 있는 하나 또는 여러 개의 파일(번들)로 결합합니다. 이 과정은 웹사이트의 로딩 시간을 줄이고 성능을 향상시키는 데 도움이 됩니다. Webpack은 다양한 타입의 파일들을 처리할 수 있는 로더 시스템을 제공합니다. 예를 들어, CSS, 이미지 파일, HTML 등을 JavaScript 모듈로 변환할 수 있습니다. 또한, 플러그인을 통해 번들링 프로세스에 커스텀 작업을 추가할 수 있습니다.

천천히 webpack의 구조와 방식에 대해서 간단히 설명을 해보겠습니다.

entry

프로젝트 내부의 모든 읽기 과정이 시작되는 위치를 의미합니다.
일반적으로 /src 의 index.ts 혹은 index.tsx를 시작되는 위치로 선정합니다.

output

프로젝트 내부의 모든 파일들의 번들된 결과물을 반환하는 파일입니다.

//webpack.config.json
...
output: {
    publicPath: '/',
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js', // 임시로 사용되는 결과 파일입니다. hash값으로 변경예정입니다.
    clean: true,
  },
  ...
  1. publicPath: 이 속성은 번들링된 파일들이 참조될 때 사용될 기본 경로를 설정합니다. 여기서 '/'로 설정된 것은 서버의 루트 경로를 기준으로 리소스를 제공하겠다는 의미입니다. 예를 들어, 서버의 루트 URL이 http://example.com/이라면, 번들링된 파일들은 http://example.com/bundle.js와 같은 방식으로 접근될 것입니다.
  2. path: 이는 Webpack이 최종적으로 번들링된 파일들을 저장할 디렉토리의 절대 경로를 설정합니다. path.resolve(dirname, 'dist')는 현재 파일이 있는 디렉토리(dirname)와 'dist' 디렉토리를 합친 절대 경로를 반환합니다. 즉, 최종적인 번들 파일은 dist 폴더 내에 저장됩니다.
  3. filename: 번들링된 결과 파일의 이름을 설정합니다. 이 예제에서는 bundle.js로 설정되어 있으며, 이는 모든 자바스크립트 파일들이 하나의 bundle.js 파일로 합쳐져 출력됨을 의미합니다.
  4. clean: 이 속성이 true로 설정되면, 새로운 빌드를 진행할 때마다 output.path에 지정된 디렉토리 내의 기존 모든 파일을 제거합니다. 이는 빌드 과정에서 생성되는 파일들이 누적되는 것을 방지하고, 항상 최신의 빌드 결과만을 유지하기 위한 설정입니다.

또한 dist폴더 내부에 bundle.js 뿐 아니라 index.html이 포함되어 있는데, 이는 output option이 아닌 plugin에서 처리해줍니다.

 new HtmlWebpackPlugin({
      template: './public/index.html',
      filename: 'index.html',
    }),

해당 방식을 통해서 index.html이 dist파일 내부에 선언됩니다. 이때 index.html이 생성되는 위치는 output 옵션의 path규칙을 따릅니다.

module의 rules

module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
	....
	]

다음과 같이 선언하여 babelrc에 작성한 내용을 바탕으로 컴파일시킨 후 번들링 시킬 수 있습니다.
babel의 관련된 옵션은 상단의 babelrc에서 처리되었기 때문에, 재 선언하지 않습니다. (하지만 해당 pr을 올릴 당시에는 이 문제를 깨닫지 못하고 중복선언 되어 있습니다. 이 문제의 처리를 위해 백로그에 기입후 pr에도 추가적으로 기록을 남겨두었습니다.)

추가적으론 tsconfig와 연결하여 alias 설정, devServer 관련설정이 있습니다

6. eslint $ prettier 설정

사실 나머지는 크게 어렵지 않은 옵션이었습니다.

// .prettierrc.json
{
  "tabWidth": 2,
  "printWidth": 80,
  "singleQuote": true,
  "endOfLine": "auto",
  "arrowParens": "always",
  "trailingComma": "es5"
}
// ts와 react, css-props 기준 필요한 옵션만을 찾아 사용했습니다. 
{
  "root": false, 
  "parser": "@typescript-eslint/parser",
  "env": {
    "browser": true,
    "node": true
  },
  "ignorePatterns": ["webpack.config.js"],
  "parserOptions": {
    "project": ["./FE/tsconfig.json"] // 진행중인 프로젝트가 모노레포로 구성되어 다음과 같은 옵션을 주어야 했습니다. 
  },
  "plugins": ["@typescript-eslint", "prettier", "unused-imports"],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking",
    "plugin:react/recommended",
    "plugin:react/jsx-runtime",
    "plugin:react-hooks/recommended",
    "prettier"
  ],
  "rules": {
    "no-unused-vars": "off",
    "@typescript-eslint/no-unused-vars": "off",
    "unused-imports/no-unused-imports": "error",
    "unused-imports/no-unused-vars": [
      "warn",
      {
        "vars": "all",
        "varsIgnorePattern": "^_",
        "args": "after-used",
        "argsIgnorePattern": "^_"
      }
    ],
    "no-console": "warn",
    "react/no-unknown-property": ["error", { "ignore": ["css"] }],
    "react/prop-types": "off"
  },
  "settings": {
    "react": {
      "version": "detect" // 자동으로 리액트 버전 감지
    }
  }
}

prettier와 eslint는 되도록 엄격하게 검사하도록 설정했습니다.

맺음말

지금까지 CRA 없이 프로젝트 셋팅에 대해서 진행해 보았습니다.
해당 코드를 작성한 PR이며,

아직 시작에 불과하지만, 프로젝트를 진행하며 webapck에 대해서 더 깊은 학습이 가능하면 좋겠습니다.

0개의 댓글