본문 바로가기

프로젝트/잔소리

[잔소리 프로젝트] CHAPTER03 | 개발 환경 설정

반응형

 


이전 이야기 | 프로젝트 계획

 

[잔소리 프로젝트] CAHTER 02 | 프로젝트 계획

이전 이야기  [잔소리 프로젝트] CHAPTER 01 | 프로젝트의 필요성들어가는 말그저 정보공유에 초점을 맞추고, 그저그런 프로그램이 아니라 현재 불편함을 경험하고 있는 사례를 토대로 프로그램

duklook.tistory.com

 

들어가는 말

이번 챕터에서는 잔소리 프로젝트을 진행하기 전 개발 환경을 설정하는 시간을 가져볼까 합니다. 사실 기본적인 환경 설정이 절반이상이고, 이전에 환경 설정 시 다루었던 부분이 대부분이기 때문에 참고한 링크나 참고할 몇 가지 정보만 제공하는 형식이 될 것 같습니다. 그래도 중요한 자료들은 모두 첨부해 두었으니 참고하셔서 본인의 상황에 맞는 프로젝트 환경을 구성해보시길 바랍니다. 

 

개발 환경 설정의 방향성(개요)

커밋 이전 유효성 검사

husky 를 적용하여 커밋 이전에 프리티어, 린트, 테스트 코드를 실행하여 통과한 경우에만 커밋이 되도록 할 생각입니다. husky 를 쓰고 안 쓰고의 차이가 컸기 때문에 개인이든 팀이든 꼭 필요한 설정이라 생각됩니다.

 

프리티어, 린트 설정

이 부분은 당연히 개발을 한다면 거의 필수적으로 해야 하는 부분이죠. 중요성은 말할 필요도 없으니 꼭 설정해줍니다. 다만, NextJS 의 경우에는 기본적으로 내장된 lint 설정이 있기 때문에, 사용자 정의가 필요한 것이 아니라면 그대로 사용하면 될 것 같습니다.

 

PWA 설정

React 같은 경우에는 별도로 PWA 설정을 하지 않아도 크롬에서 지원해주던 것으로 보이는데, NextJS 에서는 별도로 지원해주지 않는 것 같아서 next-pwa 라이브러리를 사용하여 설정해볼까 합니다. 현재 계획 중인 프로젝트의 특성이 알림과 보고서에 집중되기에 모바일 환경에서의 사용성도 중요하게 다뤄야 하는 부분 이므로 PWA 설정은 꼭 가져가야 합니다.

 

프로젝트 폴더 구조

폴더 구조를 어떻게 가져갈까 고민이 많았습니다. 아래 구조는 예시입니다. 완전히 동일할 수는 없지만, 이러한 구조를 참고해서 최대한 각 페이지 별로 컴포넌트 관리를 분리하여 일관성을 유지하고, 공통적으로 쓰이는 컴포넌트는 별도의 폴더에 보관하여 유지보수하기 좋은 형태로 가져갈까 합니다.

 

특히 이전에 services 폴더를 따로 만들고 거기서 api 요청과 관련한 로직을 get.ts, post.ts 형식으로 나눠서 보관했었는데, 이번에는 페이지 혹은 기능단위로 구분하여 처리해볼 생각입니다.

/my-nextjs-app
├── /app                   
│   ├── /api                # API 라우트
│   │   └── [...].ts        # API 라우트 파일
│   ├── /auth               # 인증 관련 페이지 및 컴포넌트
│   │   ├── login           # 로그인 페이지
│   │   │   ├── page.tsx
│   │   │   └── LoginForm.tsx # 로그인 폼 컴포넌트
│   │   └── register        # 회원가입 페이지
│   │       ├── page.tsx
│   │       └── RegisterForm.tsx # 회원가입 폼 컴포넌트
│   ├── /dashboard          # 대시보드 페이지
│   │   ├── layout.tsx
│   │   ├── page.tsx
│   │   └── /components     # 대시보드 전용 컴포넌트
│   │       ├── DashboardWidget.tsx
│   │       └── StatsCard.tsx
│   ├── /styles             # 스타일 파일
│   │   ├── globals.css
│   │   └── variables.css
│   ├── layout.tsx          # 글로벌 레이아웃
│   └── page.tsx            # 홈 페이지
├── /components             # 공용 컴포넌트
│   ├── Header.tsx
│   ├── Footer.tsx
│   └── Navbar.tsx
├── /utils                  # 유틸리티 함수
│   ├── fetcher.ts
│   └── helpers.ts
├── /public                 # 정적 파일 (이미지, 폰트 등)
│   └── favicon.ico
├── /node_modules           # npm 패키지
├── .eslintrc.json          # ESLint 설정 파일
├── .gitignore              # Git 무시 파일
├── next.config.js          # Next.js 설정 파일
├── package.json            # 프로젝트 설정 및 의존성
├── README.md               # 프로젝트 설명
└── tsconfig.json           # TypeScript 설정 파일

 

설정하기

허스키, 프리티어, 린트, vitest 설정

이 부분은 이전 복지맵 프로젝트를 할 때, 설정 과정을 남긴 포스트가 있으므로 해당 포스트의 링크를 남겨둡니다. NextJS 나 React 의 경우도 허스키, 프리티어, 테스트 부분에 있어서 큰 차이는 없으므로 저는 이를 참고하여 설정하였습니다.

 

[복지맵 프로젝트] 프로젝트 환경설정

개발 환경구성하고자 하는 프로젝트 환경은 Husky + lint-staged 를 이용한 린트 및 프리티어, vitest 자동화를 목적으로 한다. 많은 개발 블로그에서 해당 설정에 대한 내용들을 다루고 있어서 몇 몇을

duklook.tistory.com

 

단, eslint의 경우에는 NextJS 자체적으로 기본설정되어 있으므로, 별도로 설정할 필요는 없습니다. 다만 사용자 정의가 필요하다면 해당 공식 문서를 참고해주세요 https://nextjs.org/docs/pages/building-your-application/configuring/eslint

 

테스트의 경우에도 NextJS 공식 문서에서 어떻게 설정할 수 있는지 다양한 테스트 러너 별로 설명해주고 있습니다. 저의 경우에는 Vitest 부분인데, (https://nextjs.org/docs/app/building-your-application/testing/vitest) 여기를 참고하시면 좋을 것 같습니다.

 

테스트 환경 설정 관련해서 필요한 옵션이 있을지도 모릅니다. 이 때는 Vitest의 경우에는

https://vitest.dev/config/#configuration에 방문하시면 vitest.config.ts(js) 파일에서 설정할 수 있는 다양한 옵션을 설명해주고 있는 것을 볼 수 있습니다. 프로젝트 환경에 맞게 필요한 옵션을 찾아서 적용해줍시다.

 

PWA

이 부분의 경우도 가이드 자체가 잘 나와 있어서 next-pwa 공식 문서의 링크를 남겨둡니다.

 

Docs - next-pwa

Make performant web apps with Next.js & PWA.

ducanh-next-pwa.vercel.app

 

PWA 에 사용할 매니피스트 이미지 생성의 경우 아래 서비스를 추천 드립니다( https://www.pwabuilder.com/imageGenerator ).

 

 

모든 설정이 끝나고 npm run dev 를 통해 개발 서버를 실행할 때, 아래 로그가 표시된다면 성공입니다.

 

 

그리고 사이트를 접속하면 설치할 수 있는 아이콘이 표시되면, 이제 배포후에도 모바일 상에서 실제 앱처럼 설치 후 사용할 수 있게 됩니다.

 

 

모든 설정 파일

참고하실 수 있도록 제가 설정한 파일들을 모두 나열해 둡니다. 경로 표시가 안 되어 있다면 src, public 폴더와 동일한 레벨에 있다는 의미이고, src 나 특정 폴더 내부로 들어가면 경로를 표시해두었습니다. 해당 설정은 필요에 따라서 옵션이 추가될 수도 달라질 수도 있으므로 초기 설정 및 참고용으로 보면 좋을 것 같습니다.

vitest.config.ts

import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/utils/setupTests.ts'],
    coverage: {
      provider: 'istanbul',
      reporter: ['text'],
      reportsDirectory:'./tests/unit/coverage'
    },
  }
}
)

 

src/utils/setupTests.ts

import '@testing-library/jest-dom/vitest'

 

 tsconfig.json

{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@src/*": ["./src/*"],
      "@app":["./src/app"],
      "@api":["./src/app/api"],
      "@commponents":["./src/commponents"],
    }
    ,
    "types": ["vitest/globals","vitest/jsdom"]
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

 

package.json

{
  "name": "nagging",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "test": "vitest",
    "test:husky": "vitest --detectOpenHandles",
    "coverage": "vitest run --coverage",
    "lint": "next lint",
    "format": "prettier --write .",
    "prepare": "husky",
    "postinstall": "husky install"
  },
  "dependencies": {
    "@ducanh2912/next-pwa": "^10.2.7",
    "next": "14.2.4",
    "react": "^18",
    "react-dom": "^18"
  },
  "devDependencies": {
    "@testing-library/jest-dom": "^6.4.6",
    "@testing-library/react": "^16.0.0",
    "@testing-library/user-event": "^14.5.2",
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "@vitejs/plugin-react": "^4.3.1",
    "eslint": "^8.57.0",
    "eslint-config-next": "14.2.4",
    "eslint-config-prettier": "^9.1.0",
    "eslint-plugin-jsx-a11y": "^6.9.0",
    "eslint-plugin-prettier": "^5.1.3",
    "eslint-plugin-react": "^7.34.3",
    "eslint-plugin-react-hooks": "^4.6.2",
    "husky": "^9.0.11",
    "jsdom": "^24.1.0",
    "lint-staged": "^15.2.7",
    "msw": "^2.3.1",
    "postcss": "^8",
    "prettier": "^3.3.2",
    "tailwindcss": "^3.4.1",
    "typescript": "^5",
    "vitest": "^1.6.0"
  },
  "lint-staged": {
    "*.{css,ts,tsx}": [
      "npm run format",
      "eslint"
    ]
  }
}

 

src/app/manifest.json

{
    "name": "잔소리",
    "short_name": "잔소리",
    "icons": [
      {
        "src": "/icons/android-launchericon-192-192.png",
        "sizes": "192x192",
        "type": "image/png",
        "purpose": "any maskable"
      },
      {
        "src": "/icons/android-launchericon-384-384.png",
        "sizes": "384x384",
        "type": "image/png"
      },
      {
        "src": "/icons/android-launchericon-512-512.png",
        "sizes": "512x512",
        "type": "image/png"
      }
    ],
    "theme_color": "#FFFFFF",
    "background_color": "#FFFFFF",
    "start_url": "/",
    "display": "standalone",
    "orientation": "portrait"
  }

 

.prettierrc.json

{
    "singleQuote": true,
    "semi": true,
    "useTabs": false,
    "tabWidth": 2,
    "trailingComma": "all"
  }

 

.huksy/pre-commit

# .husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

echo "Current directory: $(pwd)"
npx lint-staged

 

next.config.mjs

/** @type {import('next').NextConfig} */
import withPWAInit from '@ducanh2912/next-pwa'

const withPWA = withPWAInit({
  dest:"public"
})

const nextConfig = {
  reactStrictMode: true,
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'lh3.googleusercontent.com',
      },
    ],
  },
  async headers() {
    return [
      {
        source: '/api/:path*',
        headers: [
          {
            key: 'Access-Control-Allow-Origin',
            value: process.env?.NEXT_PUBLIC_PROD_URL || 'http://localhost:3000', // 배포 주소 호스팅 가능하게
          },
          {
            key: 'Access-Control-Allow-Origin',
            value: 'https://ssl.gstatic.com/accessibility/javascript/ext/loader.js?1709880302058', // t
          },
          {
            key: 'Access-Control-Allow-Methods',
            value: 'GET, POST, PUT, DELETE, OPTIONS',
          },
          {
            key: 'Access-Control-Allow-Headers',
            value: 'Content-Type, Authorization',
          },
        ],
      },
    ]
  },
}

export default withPWA(nextConfig)

 

커밋 시도 해보기

이제 모든 설정이 끝났습니다. 예상대로의 동작이라면 커밋을 하는 순간 프리티어, 린트, 테스트 스크립트가 모두 실행되어야 합니다. 

 

정상적으로 동작을 하는지 영상으로 남겨봅니다. 결론만 말하면 npm run format 스크립트를 실행하면서 실패하였고, 이로 인해 커밋 기록이 남지 않았다는 점을 확인하였고 정상적으로 동작하는 것을 볼 수 있었습니다.

 

 

 

 

트러블 슈팅

테스트 실행하는데, @src 등의 경로를 인식하지 못해서 실패하는 경우

만일 npm run test 후 에 테스트 실패 이유가 경로를 찾지 못해서 발생하는 경우라면, vitest.config.ts 의  alias 와 tsconfig.json 의   paths  부분을아래와 같이  맞춰 줍니다. 이후 테스트를 실행하면 정상적으로 경로를 인식하는 것을 볼 수 있습니다.

 

tsconfig.json

{
  "compilerOptions": {
    "paths": {
      "@src/*": ["./src/*"],
      "@app":["./src/app"],
      "@api":["./src/app/api"],
      "@commponents":["./src/commponents"],
    }
    ,
    "types": ["vitest/globals","vitest/jsdom"]
  },
}

 

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/utils/setupTests.ts'],
    coverage: {
      provider: 'istanbul',
      reporter: ['text'],
      reportsDirectory:'./tests/unit/coverage'
    },
  },
  resolve:{
    alias: {
      "@src": path.resolve(__dirname, "./src"),
      "app": path.resolve(__dirname, "./src.app"),
      "@api": path.resolve(__dirname, "./src/app/api"),
      "@commponents": path.resolve(__dirname, ".src/components"),
    },
  }
}
)

/**
 *       "@src/*": ["./src/*"],
      "@app":["./src/app"],
      "@api":["./src/app/api"],
      "@commponents":["./src/commponents"],
 * 
 */

 

next lint 실행히 실패되고, app, pages 디렉토리 누락 문제가 발생하는 경우

해당 경우는 아래 스택오버플로우를 참고하시면 됩니다.

https://stackoverflow.com/questions/68419192/nextjs-linter-with-lint-staged-not-working

 

나가는 말

오늘은 잔소리 프로젝트를 본격적으로 시작하기 전에 개발 환경을 설정하는 시간을 가져보았습니다. 이번 프로젝트가 어느 정도 걸리지는 모르겠지만 일단 갖춰둘 필요가 있는 모든 환경에 대해서 설정을 해보았구요. 이 외에도 필요한 설정이 있다면 개발을 하는 도중에도 계속 추가될 수 있겠죠.

 

제가 상세하게 포스팅한 글은 아니지만 제가 참고한 문서들은 모두 링크로 남겨두었습니다. 여러모로 도움이 되셨다면 좋겠네요. 이만 글을 줄이도록 하겠습니다.

반응형