Dog foot print

React resolve + css/sass + typescript 적용하기 본문

REACT

React resolve + css/sass + typescript 적용하기

개 발자국 2023. 3. 15. 09:20

기본 폴더 구조 및 파일 구조

Note : /src/Components/Header.jsx , /src/Utils/Math.ts, /src/style/main.css , /src/Utils/index.ts , /src/UI/Button.tsx 를 제외한 파일들은 이전 포스팅에서 내용을 확인해주세요

 

Note : 이 포스팅은 webpack-dev-server 사용 할 수 있도록 webpack.config.js와 의존성이 일부 설치되어 있습니다. 이전 포스팅에서 내용을 확인해주세요.

 

폴더 구조

.
├── babel.config.js
├── dist
│ ├── bundle.js
│ └── index.html
├── package.json
├── public
│ └── index.html
├── src
│ ├── App.jsx
│ ├── UI
│ │ └── Button.tsx
│ ├── Utils
│ │ ├── Math.ts
│ │ └── index.ts
│ ├── Components
│ │ └── Header.jsx
│ ├── constants
│ ├── index.js
│ └── style
│ └── main.css
├── webpack.config.js
└── yarn.lock

 

src/Utils/Math.ts

export function add(a: number, b: number) {
    return a + b
}

src/Utils/index.ts

export * from "./Math"

src/UI/Button.tsx

import React from 'react'

export default function Button() {
  return (
    <button>Hello world</button>
  )
}

src/style/main.css

div{
    color : blue
}

src/Components/Header.jsx

import React from 'react'

export default function Header() {
  return (
    <div>Header</div>
  )
}

Resolve

Webpack의 Resolve 옵션은 모듈을 불러 올 때, 어디서 어떻게 찾는가? 에 대한 옵션이다. 예를 들어 우리는 모듈을 불러 올 때, 상대경로로 파일을 작성한다. 그러나, 프로젝트 규모가 커지고 폴더들이 분산되어 버린다면, 모듈을 찾기위해 ../../../ 이렇게 작성한다면, 매우 폭력적인 코드가 될것이다. 이를 위해, Resolve 옵션은 alias와 같은 기능을 통해 특정 문자열을 특정경로로 해석 해주는 기능을 제공한다.

alias

우리는 현재 위치를 기준해서, 특정 파일 혹은 폴더까지 경로를 상대경로 라고 한다. 그리고 컴퓨터 루트 폴더에서 특정 파일 혹은 폴더까지 경로를 절대경로라고 한다.

상대경로를 사용하는 경우, 바로 같은 폴더내에 위치하는 파일을 찾거나 하위 폴더에 위치한 파일을 찾는 것은 크게 불편하지 않다. 그러나, 찾는 파일이나 폴더가 현재 위치보다 상위 폴더에 위치하고 있을 때, 상위 폴더로 이동하기 위해서 ../ 를 사용해야 한다.

문제는 상위 폴더로 이동이 많아 질수록 ../ 를 많이 사용해야 하며, 현재 파일의 위치가 변경되면 이 경로 또한 변경되어야 한다.

alias 옵션은 특정 문자열을 특정 경로로 변경해 줄 수 있다. 이말인 즉 특정 문자열을 절대경로로 변경이 가능하다는 것이다.

현재 ‘src’ 폴더내에 폴더는 components, style, ui, utils가 존재하니 이를 alias를 이용해서 경로를 지정해보자.

const path = require("path");
...
module.export = {
    ...
     resolve: {
        alias: {
            "@Components": path.resolve(__dirname, 'src/Components/'),
            "@UI": path.resolve(__dirname, 'src/UI/'),
            "@Utils": path.resolve(__dirname, 'src/Utils/'),
            "@style": path.resolve(__dirname, 'src/style/')
        }
    },
    ...
}

alias옵션을 설정하는 것은 매우 간단하다. resolve 옵션을 생성하고 alias객체를 생성 한 뒤, 경로를 지칭할 key와 경로를 설정해주면 된다. alias 옵션으로 설정된 경로는 ‘at’의 의미를 담아 “@”를 접두사로 주로 사용한다. 이제 이를 이용하여, App.jsx 에서 /src/Components/Header.jsx를 불러보자.

/src/App.jsx

import React from 'react'
import Header from "@Components/Header.jsx"
export default function App() {
    return (
        <div>
            <Header></Header>
        </div>
    )
}

브라우저 렌더링 결과

성공적으로 App.jsx 에서 Header.jsx를 사용 할 수 있다.

Extensions

Css와 같은 파일들은 자주 임포트하지 않으니, 확장자를 사용하는 것은 감수 할 수 있다. 그러나 스크립트 파일은 자주 사용해야 하다보니, js,jsx,ts,tsx 를 사용하는 것은 매우 귀찮은 일이 될 수 있다.

extensions 옵션은 확장자를 가지지 않은 경우, 내가 지정한 순서에 따라, 해당 폴더내에서 확장자를 부여하며 같은이름의 파일을 찾을 수 있다

webpack.config.js

module.exports = {
     resolve: {
        extensions : [".js",".jsx",".ts",".tsx"],
        alias: {...}
    },
}

src/App.jsx

import React from 'react'
import Header from "@Components/Header"
export default function App() {
    return (
        <div>
            <Header></Header>
        </div>
    )
}

브라우저 렌더링 결과

이제 확장자 없이 경로를 지정하여도, 파일을 찾을 수 있다.

css 사용해보기

Src/index.js

import "@style/main.css"

css를 import 하기 위해서는 css-loader 가 필요하다.

$: yarn add -D css-loader

webpack.config.js

module.export = {
  ...
    module: {
        rules: [
            ...
           {
                test: /\.css$/i,
                use: ["css-loader"],
            },
        ],
    },
    ...
}

여기까지 진행 했지만 파일이 생성되거나, 어떤 변화도 존재하지 않는다. 그 이유인 즉css-loader 는 ‘.js’ 파일에서 css파일을 가져오는 역할만 하기 때문이다. Import 되어 있는 css를 사용하기 위해서는 style-loader 혹은 MiniCssExtractPlugin 이 필요하다.

$: yarn add style-loader mini-css-extract-plugin -D

먼저 style-loader는 임포트되어 있는 css를 렌더링 되는 html에서 style태그를 만들어, 스타일을 사용 할 수 있게한다. 사용 방법은 use 배열에 style-loader 입력하면 된다.

webpack.config.js

module.export = {
  ...
    module: {
        rules: [
            ...
           {
                test: /\.css$/i,
                use: ["style-loader","css-loader"],
            },
        ],
    },
    ...
}

이렇게 작성하고, $: yarn dev 명령어로 확인해보면 다음과 같이 임포트 된 css가 스타일 태그로 변경된 결과를 볼 수 있다.

브라우저 렌더링 결과 및 스타일 태그

이번에는 스타일 태그가 아닌 css 파일로 제공 해줄 수 있도록 mini-css-extract-plugin을 사용해보자.

webpack.config.js

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
...

module.export = {
  ...
    module: {
        rules: [
            ...
           {
                test: /\.css$/i,
                use: [MiniCssExtractPlugin.loader,"css-loader"],
            },
        ],
    },
    plugins: [
        ...
        new MiniCssExtractPlugin()
    ],

    ...
}

위와 같이 작성하고, 다시 $: yarn dev 명령어를 실행해보면 아래와 같이 html파일에 main.css가 링크되고, dist폴더에는 ‘main.css’라는 파일이 생성된다.

브라우저 렌더링 결과 및 css

sass 사용해보기

$: yarn add sass-loader sass

sass-loader : sass를 css로 변환해주는 모듈
sass : sass를 해석하기 위한 모듈

webpack.config.js

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
...

module.export = {
  ...
    module: {
        rules: [
            ...
            {
                test: /\.s[ac]ss$/i,
                use: [
                    // css file gen
                    MiniCssExtractPlugin.loader,
                    // Translates CSS into CommonJS
                    "css-loader",
                    // Compiles Sass to CSS
                    "sass-loader",
                ],
            },
        ],
    },
    ...
}

src/components/Header.jsx

import React from 'react'

export default function Header() {
  return (
    <div>
      <h1 className='title'>WOW</h1>
      <h2 className='sub-title'>H2</h2>
    </div>
  )
}

src/style/main.css -> /src/style/main.scss

포맷 변경

div{
    .title{
        color: red;
    }

    .sub-title{
        color : blue
    }
}

Header.jsx와 main.scss를 변경하면 다음과 같은 렌더링 결과와 scss의 결과물을 볼 수 있다.

typescript 적용하기

$ : yarn add typescript ts-loader @types/react @types/react-dom -D

ts-loader : 웹팩에서 typescript를 사용 할 수 있게 해주는 모듈
typescript : typescript를 javascript로 변환시켜주는 모듈
@types/react : react type
@types/react-dom : react-dom type

ts파일을 웹팩으로 번들링 하기위해서, 아래와 같이 로더를 적용 시켜주고 js파일을 ts로 변환하기 위해서, entry를 index.js를 index.tsx로 변환합니다.

/webpack.config.js

...
const javascriptRegex = /\.(jsx|js)$/;
const typescriptRegex = /\.(ts|tsx)$/;
...
module.export = {
    ...
  entry: "./src/index.tsx", // main
    module : {
        rules : [
            ...
                {
                test: javascriptRegex,
                exclude: /node_modules/,
                use: {
                    loader: "babel-loader",
                },
            },
            {
                test: typescriptRegex,
                exclude: /node_modules/,
                use: ["ts-loader"]
            },
            ...
        ]    
    }
}

typescript를 처리 할 수 있도록 간단하게 tsconfig.json파일을 생성해줍니다.

/tsconfig.json

{
    "compilerOptions": {
      "outDir": "./dist/",
      "noImplicitAny": true,
      "module": "es6",
      "target": "es5",
      "jsx": "react",
      "allowJs": true,
      "moduleResolution": "node"
    }
}

이제 index.js를 index.tsx로 변환하고 jsx파일들을 tsx파일들로 포맷을 변환시켜 줍니다.

 

Index.js -> index.tsx
Header.jsx -> Header.tsx
App.jsx -> App.tsx

 

Tip : index.js를 .ts포맷이 아닌 tsx포맷으로 변경하는 이유는 현재 index.js에서 jsx문법을 사용했기 떄문입니다. typescript를 설치한 지금 typescript의 규칙을 따라야합니다.

 

여기까지 하고 실행을 시작하면 아래와 같이 분명 번들링은 되었음에도, 에러메세지가 등장 할 것이다. 또한 에디터에서 @Components/Header가 무엇이냐고 비명을 지를 것이다.

 

 

이 문제는 typescript를 적용하면서, typescript에게 alias된 경로를 알려주지 않았기 때문이다. 이는 typescript가 사용될 수 있는 javascript로 변환 되는 과정만을 행하고 import 되어 있는 모듈을 연결하는 일은 번들러가 담당하기 때문에 발생하는 것이다.

그렇기에 tsconfig.json 에서 alias된 값들을 명시하고 baseUrl위치 기준에서 작성해야 한다.

 

tsconfig.json

{
  "compilerOptions": {
    ...
      "paths" : {
        "@Components/*" : ["src/Components/*"],
        "@Utils/*" : ["src/Utils/*"],
        "@UI/*" : ["src/UI/*"],
        "@style/*" : ["src/style/*"]
      }
  },
    ...
}

이제 App.tsx에서 Header를 사용하는데, 문제가 없을 것이다. 추가로 Button.tsx도 변경하여, 사용해보자.

 

src/App.tsx

import React from 'react'
import Header from "@Components/Header"
import Button from '@UI/Button'

export default function App() {

    const buttonAttributes = {
        onClick : () => alert("???")
    }

    return (
        <div>
            <Header></Header>
            <Button attribute={buttonAttributes}>
                hi
            </Button>
        </div>
    )
}

src/UI/Button.tsx

import React, { ReactNode} from 'react'

type JSX_ElementKey =  keyof JSX.IntrinsicElements

interface ElementProps<T extends JSX_ElementKey> {
   attribute? : JSX.IntrinsicElements[T],
   children :ReactNode,
}

export default function Button(props? : ElementProps<"button">) {

  const {attribute = {},children} = props

  return (
    <button {...attribute} >
      {children}
    </button>
  )
}

 

 

폴더 index 탐색

onClick을 이전에 만들어놓았던 add함수를 이용을 해보자. 폴더 구조만 명시 했을 경우에는 자동으로 index라는 파일을 찾게 된다.

src/App.tsx

import React from 'react'
import Header from "@Components/Header"
import Button from '@UI/Button'
import { add } from "@Utils"

export default function App() {

    const buttonAttributes = {
        onClick : () => alert(add(1,1))
    }

    return (
        <div>
            <Header></Header>
            <Button attribute={buttonAttributes}>
                hi
            </Button>
        </div>
    )
}

이를 yarn dev 명령어를 이용해서, 실행하니 또 아래와 같이 에러를 발생시키지만, 코드는 작동한다.

 

이번에는 뭔가 느낌이 올 것이다. “아 … 이건 typescript에서 문제가 발생하는 구나 …” 그렇다.

 

tsconfig.json

{
  "compilerOptions": {
      ...
      "paths" : {
        "@Components/*" : ["src/Components/*"],
        "@Utils/*" : ["src/Utils/*"],
        "@UI/*" : ["src/UI/*"],
        "@style/*" : ["src/style/*"]
      }
  },
    ...
}

여기서 문제는 @Utils/*@Utils와 동일하지 않기 때문에 발생한 것이다. alias를 명시한 경로에서 index파일을 찾으려면 이를 명시해주어야 한다. 아래와 같이 @Utils 항목을 생성해주자.

 

tsconfig.json

    {
  "compilerOptions": {
      ...
      "paths" : {
        "@Components/*" : ["src/Components/*"],
        "@Utils/*" : ["src/Utils/*"],
          "@Utils" : ["src/Utils/index"],
        "@UI/*" : ["src/UI/*"],
        "@style/*" : ["src/style/*"],
      }
  },
    ...
}

이제 실행하였을 때 에러를 발생시키지 않는다.

반응형
Comments