1. 통합개발환경 세팅

통합개발환경 세팅을 해보자

  • TypeScript

  • ESLint

  • React

  • Parcel

  • React Router

  • styled-components

  • styled-reset

  • usehooks-ts

  • usestore-ts

    • Axios

    • tsyringe

    • reflect-metadata

  • jest-dom

    • MSW

  • CodeceptJS

순서대로 설치 및 테스트 진행

1. 프로젝트 폴더 생성 및 진입

mkdir shop
cd shop

2. npm 글로벌 설치

npm install -g npm

3. Node 버전 잡기

node --version > .node-version

4. npm 패키지 초기화

npm init -y

5. TS 및 ESLint 설치

Typescript 설치

npm i -D typescript

Typescript 설정 파일 생성

npx tsc --init

tsconfig.json 파일에서 jsx 항목 수정

{
  "jsx": "react-jsx"
}

ESLint 설치

npm i -D eslint

ESLint 설정

npx eslint --init

$ > To check syntax, find problems, and enforce code style
$ > JavaScript modules (import/export)
$ > React
$ > Yes(TypeScript)
$ > Browser
$ > Use a popular style guide
$ > XO
$ > JavaScript
$ > Yes
$ > npm

ESLint Airbnb 스타일 적용

Airbnb 스타일 다시 적용(TS에서 Airbnb 지원하지 않기 때문)

npm uninstall eslint-config-xo
npm uninstall eslint-config-xo-typescript

npm i -D eslint-config-airbnb
npm i -D eslint-plugin-import
npm i -D eslint-plugin-react
npm i -D eslint-plugin-react-hooks
npm i -D eslint-plugin-jsx-a11y

.eslintrc.js 파일 수정

module.exports = {
  env: {
    browser: true,
    es2021: true,
    jest: true,
  },
  extends: [
    'airbnb',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended',
    'plugin:react/jsx-runtime',
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  plugins: ['react', 'react-hooks', '@typescript-eslint'],
  settings: {
    'import/resolver': {
      node: {
        extensions: ['.js', '.jsx', '.ts', '.tsx'],
      },
    },
  },
  rules: {
    indent: ['error', 2],
    'no-trailing-spaces': 'error',
    curly: 'error',
    'brace-style': 'error',
    'no-multi-spaces': 'error',
    'space-infix-ops': 'error',
    'space-unary-ops': 'error',
    'no-whitespace-before-property': 'error',
    'func-call-spacing': 'error',
    'space-before-blocks': 'error',
    'keyword-spacing': ['error', { before: true, after: true }],
    'comma-spacing': ['error', { before: false, after: true }],
    'comma-style': ['error', 'last'],
    'comma-dangle': ['error', 'always-multiline'],
    'space-in-parens': ['error', 'never'],
    'block-spacing': 'error',
    'array-bracket-spacing': ['error', 'never'],
    'object-curly-spacing': ['error', 'always'],
    'key-spacing': ['error', { mode: 'strict' }],
    'arrow-spacing': ['error', { before: true, after: true }],
    'import/no-extraneous-dependencies': [
      'error',
      {
        devDependencies: [
          '**/*.test.js',
          '**/*.test.jsx',
          '**/*.test.ts',
          '**/*.test.tsx',
        ],
      },
    ],
    'import/extensions': [
      'error',
      'ignorePackages',
      {
        js: 'never',
        jsx: 'never',
        ts: 'never',
        tsx: 'never',
      },
    ],
    'react/jsx-filename-extension': [
      2,
      {
        extensions: ['.js', '.jsx', '.ts', '.tsx'],
      },
    ],
    'jsx-a11y/label-has-associated-control': ['error', { assert: 'either' }],
  },
};

prettier 충돌 방지

npm install --save-dev eslint-config-prettier

.eslintrc.js 파일 수정

extends: [
  'airbnb',
  'plugin:@typescript-eslint/recommended',
  'plugin:react/recommended',
  'plugin:react/jsx-runtime',
  'prettier',
],

.eslintignore 파일 생성

/node_modules/
/dist/
/.parcel-cache/

6. Jest 설치

npm i -D jest @types/jest
npm i -D @swc/core @swc/jest jest-environment-jsdom
npm i -D @testing-library/react @testing-library/jest-dom@5.16.4

jest.config.js

jest.config.js 생성

module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
  transform: {
    '^.+\\.(t|j)sx?$': [
      '@swc/jest',
      {
        jsc: {
          parser: {
            syntax: 'typescript',
            jsx: true,
            decorators: true,
          },
          transform: {
            react: {
              runtime: 'automatic',
            },
            legacyDecorator: true,
            decoratorMetadata: true,
          },
        },
      },
    ],
  },
};

React 설치

npm i react react-dom
npm i -D @types/react @types/react-dom

기본 파일 생성

  • index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="./src/main.tsx"></script>
  </body>
</html>
  • src/main.tsx

import ReactDOM from 'react-dom/client';

import App from './App';

function main() {
  const element = document.getElementById('root');

  if (!element) {
    return;
  }

  const root = ReactDOM.createRoot(element);

  root.render(<App />);
}

main();
  • src/App.tsx

export default function App() {
  return <div>Hello, world!</div>;
}

Parcel 설치

npm i -D parcel

parcel-reporter-static-files-copy 설치

npm install -D parcel-reporter-static-files-copy

static folder 생성

Root path 에 static 폴더를 만든다. 정적 이미지 등을 해당 폴더에 넣는다.

mkdir static

.gitignore 파일 생성

  • VisualStudioCode, react, macOS, Node 키워드로 생성(gitignore.io)

아래의 폴더는 반드시 추가할 것

...
/dist/
/.parcel-cache/

package.json 수정

"source": "./index.html",
...
...
"scripts": {
    "start": "parcel --port 8080",
    "build": "parcel build",
    "check": "tsc --noEmit",
    "lint": "eslint --fix --ext .js,.jsx,.ts,.tsx .",
    "test": "jest",
    "coverage": "jest --coverage --coverage-reporters html",
    "watch:test": "jest --watchAll"
  },

실행 테스트

# 로컬 실행
npm run start

# build
npx parcel build

# 정적 서버 실행
npx servor ./dist

MSW 설치 및 관련 파일 생성

TODO: 추후 보완할 것

  • src/setupTests.ts

  • src/mocks/server.ts

  • src/mocks/handler.ts 파일 추가

jest.config.js 설정 변경

TODO: 추후 보완할 것

setupFilesAfterEnv 의 속성에 setupTests.ts 파일 추가


React Router 설치

npm install react-router-dom

styled-components 설치

npm i styled-components@5.3.10
npm i -D @types/styled-components @swc/plugin-styled-components
npm i styled-reset

/src/.swcrc 파일 생성

{
  "jsc": {
    "experimental": {
      "plugins": [
        [
          "@swc/plugin-styled-components",
          {
            "displayName": true,
            "ssr": true
          }
        ]
      ]
    }
  }
}

main.tsx 수정

import React from 'react';

import ReactDOM from 'react-dom/client';

import App from './App';

function main() {
  const element = document.getElementById('root');

  if (!element) {
    return;
  }

  const root = ReactDOM.createRoot(element);

  root.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
  );
}

main();

App.tsx 수정

import { RouterProvider, createBrowserRouter } from 'react-router-dom';

import routes from './routes';

const router = createBrowserRouter(routes);

export default function App() {
  return <RouterProvider router={router} />;
}

/src/routes.tsx 파일 생성 /src/components/Layout.tsx 파일 생성 /src/pages/HomePage.tsx 파일 생성

import Layout from './components/Layout';
import HomePage from './pages/HomePage';

const routes = [
  {
    element: <Layout />,
    children: [{ path: '/', element: <HomePage /> }],
  },
];

export default routes;

/src/styles 폴더 생성

  • /src/styles/defaultTheme.ts 파일 생성

    const defaultTheme = {
      colors: {
        background: '#FFFFFF',
        text: '#000000',
        primary: '#F00000',
        secondary: '#00FFFF',
      },
    };
    
    export default defaultTheme;
  • /src/styles/Theme.ts 파일 생성

    import defaultTheme from './defaultTheme';
    
    type Theme = typeof defaultTheme;
    
    export default Theme;
  • /src/styles/styled.d.ts 파일 생성

    import 'styled-components';
    
    import Theme from './Theme';
    
    declare module 'styled-components' {
      export interface DefaultTheme extends Theme {}
    }
  • /src/styles/GlobalStyle.ts 파일 생성

    import { createGlobalStyle } from 'styled-components';
    
    const GlobalStyle = createGlobalStyle`
      html {
        box-sizing: border-box;
      }
    
      *,
      *::before,
      *::after {
        box-sizing: inherit;
      }
    
      html {
        font-size: 62.5%;
      }
    
      body {
        font-size: 1.6rem;
        background: ${(props) => props.theme.colors.background};
        color: ${(props) => props.theme.colors.text}
      }
    
      :lang(ko) {
        h1, h2, h3 {
          word-break: keep-all;
        }
      }
    `;
    
    export default GlobalStyle;
  • App.tsx 수정

import { RouterProvider, createBrowserRouter } from 'react-router-dom';

import routes from './routes';

import { ThemeProvider } from 'styled-components';
import defaultTheme from './styles/defaultTheme';
import { Reset } from 'styled-reset';
import GlobalStyle from './styles/GlobalStyle';

const router = createBrowserRouter(routes);

export default function App() {
  return (
    <ThemeProvider theme={defaultTheme}>
      <Reset />
      <GlobalStyle />
      <RouterProvider router={router} />
    </ThemeProvider>
  );
}

helper 만들어주기

/src/utils/test-helpers.tsx

/* eslint-disable import/prefer-default-export */

// eslint-disable-next-line import/no-extraneous-dependencies
import { render as originalRender } from '@testing-library/react';

import React from 'react';

import { MemoryRouter } from 'react-router-dom';

import { ThemeProvider } from 'styled-components';

import defaultTheme from '../styles/defaultTheme';

type Option = {
  path?: string;
};

export function render(
  element: React.ReactElement,
  { path = '/' }: Option = {},
) {
  return originalRender(
    <MemoryRouter initialEntries={[path]}>
      <ThemeProvider theme={defaultTheme}>{element}</ThemeProvider>
    </MemoryRouter>,
  );
}

Axios 설치

npm i axios

tsyringe, reflect-metadata, usestore-ts 설치

npm i tsyringe reflect-metadata usestore-ts

/src/main.tsx, src/setupTests.ts 파일 수정

// 최상단에 임포트(tsyringe 의존성)
import 'reflect-metadata';

tsconfig.json 수정

...
{
  "experimentalDecorators": true
  "emitDecoratorMetadata": true
}
...

jest.config.js 파일 수정

module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
  transform: {
    '^.+\\.(t|j)sx?$': [
      '@swc/jest',
      {
        jsc: {
          parser: {
            syntax: 'typescript',
            jsx: true,
            decorators: true,
          },
          transform: {
            react: {
              runtime: 'automatic',
            },
            legacyDecorator: true, // 이거
            decoratorMetadata: true, // 이거
          },
        },
      },
    ],
  },
};

CodeceptJS 설치

npx codeceptjs init

# > Do you plan to write tests in TypeScript? y
# > Where are your tests located? ./tests/**/*_test.js
# > What helpers do you want to use? Playwright
# > where should logs, screenshots, and reports to be stored? (./output)
# > ? [Playwright] Browser in which testing will be performed. Possible options: chromium, firefox, webkit or electron? chromium
# > Do you want localization for tests? English (no localization)
# > [Playwright] Base url of site to be tested (http://localhost)
# > [Playwright] Show browser window n
# > Feature which is being tested (ex: account, login, etc)
# > Filename of a test (_test.js)

부가 설정

npm i -D eslint-plugin-codeceptjs playwright @codeceptjs/configure
  • codecept.conf.ts 에서 config의 타입 제거 및 수정.

    import { setCommonPlugins, setHeadlessWhen } from '@codeceptjs/configure';
    
    setHeadlessWhen(process.env.HEADLESS);
    
    setCommonPlugins();
    
    export const config = {
      name: 'my-project',
      tests: './tests/**/*_test.ts',
      output: './output',
      helpers: {
        Playwright: {
          browser: 'chromium',
          url: 'http://localhost',
          show: false,
        },
      },
      include: {
        // I: './steps_file', to remove
        I: './tests/steps_file',
      },
      // added
      plugins: {
        retryFailedStep: {
          enabled: true,
        },
      },
    };
  • tests/steps.d.ts 파일 생성

    // <reference types='codeceptjs' />
    
    type steps_file = typeof import('./steps_file');
    
    declare namespace CodeceptJS {
      interface SupportObject {
        I: I;
      }
    
      interface Methods extends Playwright, REST, JSONResponse {}
    
      interface I extends ReturnType<steps_file> {}
    
      namespace Translation {
        interface Actions {}
      }
    }
  • steps_files.js 파일 tests 폴더로 이동

  • tsconfig.json 파일 수정

    • CodeceptJS가 내부적으로 ts-node 사용하기 때문에

    {
      {
        ...
        /* Completeness */
        // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
        "skipLibCheck": true /* Skip type checking all .d.ts files. */
      },
      "ts-node": {
        "files": true
      }
    }
  • /tests/.eslintrc.js 파일 생성

    module.exports = {
      extends: ['plugin:codeceptjs/recommended'],
    };
  • .gitignore 에 output/ 추가

  • package.json 수정

{
  "scripts": {
    "start": "parcel --port 8080",
    "build": "parcel build",
    "serve": "servor dist index.html 8000 --reload",
    "check": "tsc --noEmit",
    "lint": "eslint --fix --ext .js,.jsx,.ts,.tsx .",
    "test": "jest",
    "coverage": "jest --coverage --coverage-reporters html",
    "watch:test": "jest --watchAll",
    "codeceptjs": "codeceptjs run --steps",
    "codeceptjs:headless": "HEADLESS=true codeceptjs run --steps"
  }
}

Last updated