무엇을 설치하고
어떤 명령을 쓸지 정한다
npm이 읽는 유일한 필수 파일이다. 여기 적힌 네 개의 script와 두 종류의 의존성이 개발·검사·빌드·배포의 입구가 된다.
이 파일을 만든 이유
JavaScript 프로젝트에서 이 파일은 신분증이자 목록이다. 어떤 package가 필요한지, 어떤 명령으로 실행하는지가 여기에만 적힌다. npm install은 이 파일을 읽어 node_modules/를 만들고, Dockerfile은 이 파일을 먼저 복사해 layer cache를 얻는다.
동시에 협업의 계약이기도 하다. 다른 사람이 저장소를 받아 npm install만 하면 같은 환경이 만들어져야 한다. 그 재현성을 실제로 보장하는 것은 짝을 이루는 package-lock.json이다.
전체 코드
{
"name": "fe",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "oxlint",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.8",
"react-dom": "^19.2.8"
},
"devDependencies": {
"@types/node": "^24.13.3",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@vitejs/plugin-react": "^6.1.0",
"oxlint": "^1.79.0",
"typescript": "~6.0.2",
"vite": "^8.2.2"
}
}
상단 네 항목
"name": "fe" · "version": "0.0.0"
npm에 공개할 때 쓰는 이름과 버전이다. 이 프로젝트는 공개하지 않으므로 생성 당시의 기본값이 그대로 남아 있다. 동작에는 영향이 없다.
"private": true
실수로 npm publish를 실행해도 거부된다. 애플리케이션은 배포용 package가 아니므로 안전장치로 켜 둔다.
"type": "module"
이 폴더의 .js 파일을 ES module로 해석하라는 선언이다. import/export 문법이 기본이 되고 require()는 쓸 수 없다. vite.config.ts가 import로 시작할 수 있는 이유이며, index.html의 <script type="module">과도 같은 방향이다.
scripts — 네 개의 입구
| 명령 | 실행되는 것 | type 검사 | 언제 쓰는가 |
|---|---|---|---|
npm run dev | vite | 안 함 | 개발. 5173 포트, 저장하면 즉시 반영 |
npm run build | tsc -b && vite build | 함 | 배포용 dist/ 생성 |
npm run lint | oxlint | 안 함 | 코드 규칙 검사 |
npm run preview | vite preview | 안 함 | 만들어진 dist/를 확인 |
tsc -b && vite build — 두 명령인 이유
Vite는 type을 검사하지 않고 지우기만 한다. 빠른 대신 잘못된 type을 그냥 통과시킨다. 그래서 tsc -b를 앞에 두어 먼저 검사한다.
&&는 앞 명령이 성공해야 뒤를 실행한다. type 오류가 있으면 dist/가 아예 만들어지지 않으므로, 깨진 결과물이 Docker 이미지로 들어가는 일이 없다. -b는 build mode로, tsconfig.json의 project references를 따라간다.
oxlint
Rust로 작성된 빠른 linter다. 설정은 fe/.oxlintrc.json에 있고 두 규칙을 켜 두었다.
react/rules-of-hooks: error는 Hook 호출 규칙을 강제한다. useState를 조건문이나 반복문 안에서 부르면 React가 state를 잘못 짝지어 원인을 찾기 어려운 버그가 생긴다. 이 규칙은 그것을 미리 막는다.
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "oxc"],
"rules": {
"react/rules-of-hooks": "error",
"react/only-export-components": ["warn", { "allowConstantExport": true }]
}
}
$schema는 편집기가 이 파일의 항목을 자동완성하게 해 주는 참조이며 lint 동작에는 영향이 없다. plugins로 React·TypeScript 규칙 묶음을 켜고, rules에서 두 가지만 명시했다. only-export-components가 warn인 것은 Fast Refresh가 잘 동작하려면 한 파일이 컴포넌트만 내보내는 편이 좋다는 권고이기 때문이다. allowConstantExport: true 덕분에 상수를 함께 내보내는 것은 허용된다.
다만 npm run build에는 lint가 포함되지 않는다. 따로 실행해야 하며, Dockerfile도 실행하지 않는다.
의존성 — 두 종류로 나누는 이유
| 구분 | 내용 | production image에 |
|---|---|---|
dependencies | react, react-dom | 번들에 포함되어 브라우저로 간다 |
devDependencies | typescript, vite, oxlint, @types/*, plugin | 결과물에 없다. 만들 때만 쓰인다 |
이 구분이 multi-stage Dockerfile과 짝을 이룬다. build 단계에서는 둘 다 필요하지만, production 단계는 Nginx와 dist/만 복사하므로 두 목록 모두 최종 이미지에 남지 않는다. 26 MB라는 크기가 나오는 이유다.
@types/react가 devDependencies인 이유
type 정의는 검사할 때만 필요하고 실행 코드에는 남지 않는다. 브라우저로 가는 것은 react뿐이다.
^와 ~의 차이
^19.2.8은 19.x.x 안에서 최신을 허용하고, ~6.0.2는 6.0.x 안에서만 허용한다. TypeScript에만 ~를 쓴 것은, 부버전이 올라갈 때 새 검사가 추가되어 기존 코드가 갑자기 컴파일되지 않는 일이 실제로 있기 때문이다.
그래도 재현성은 lock 파일이 보장한다
^는 범위일 뿐이다. 실제로 어떤 버전이 설치됐는지는 package-lock.json에 정확히 적힌다. Dockerfile이 npm install이 아니라 npm ci를 쓰는 이유가 이것이다. npm ci는 lock 파일만 보고 설치하며, lock과 package.json이 어긋나면 고치지 않고 실패한다.
실제로 확인하는 방법
cd fe
npm ci # lock 파일 그대로 설치
npm run lint # 규칙 검사
npm run build # tsc -b 후 vite build
npm run preview # 만들어진 dist 를 확인
| 확인 | 기대 결과 |
|---|---|
npm ci | package-lock.json과 다르면 설치하지 않고 실패한다 |
npm run build | type 오류가 있으면 dist/가 생기지 않는다 |
| 의존성 구분 | npm ls react는 보이고, 만들어진 dist/에 typescript는 없다 |
| Docker | Dockerfile이 이 파일과 lock 파일만 먼저 복사해 cache를 얻는다 |
자주 발생하는 오류
| 증상 | 원인 | 진단 |
|---|---|---|
npm ci 실패 | package.json만 고치고 lock 갱신 안 함 | 로컬에서 npm install로 lock을 갱신하고 함께 commit한다 |
Cannot use import statement | "type": "module" 누락 | ES module 선언이 필요하다 |
| build만 실패 | tsc -b의 type 오류 | npm run dev는 검사하지 않으므로 여기서 처음 드러난다 |
| lint 규칙이 안 걸림 | build에 lint가 없음 | npm run lint를 따로 실행한다 |
| 설치 후 동작이 다름 | ^ 범위 안에서 다른 버전이 설치됨 | lock 파일을 공유하고 npm ci를 쓴다 |
설계 선택과 대안
build에 lint를 넣어야 하지 않나?
넣으면 규칙 위반이 이미지 생성을 막아 안전하다. 대신 build가 느려지고, 형식 문제로 배포가 막히는 상황이 생긴다. 11단계의 통합 test에서 CI를 정할 때 함께 결정할 문제다.
name과 version을 고쳐야 하나?
공개하지 않으므로 동작에는 영향이 없다. 다만 여러 package로 나뉘는 구조가 되면 이름이 실제 식별자가 된다. 지금은 private: true가 실수를 막아 준다.
Yarn이나 pnpm은?
pnpm은 디스크를 크게 아껴 8GB·저장공간 제약 환경에 유리하다. 다만 package-lock.json과 Dockerfile의 npm ci를 함께 바꿔야 한다. 지금은 도구를 하나로 유지하는 편이 학습에 방해가 적다.
이전 단계와 다음 파일
여기 적힌 tsc -b가 읽는 설정은 tsconfig.json, vite build가 읽는 설정은 vite.config.ts에 있다. 이 파일을 이용해 이미지를 만드는 과정은 Dockerfile에서 이어진다.