본문으로 건너뛰기

개발 환경

본 문서는 프로젝트 구성 파일의 역할과 실행 명령을 정의합니다. 각 규칙의 근거는 해당 참조 문서가 원본이며, 본 문서는 그 규칙이 어디에서 강제되는지를 연결합니다.

1. 구성 파일

파일 역할 규칙의 원본
package.json 의존성과 버전 본 문서에 버전을 중복 기재하지 않습니다
tsconfig.json 컴파일러 옵션, 경로 별칭 패키지 배치와 참조 규칙 7.3절
angular.json 빌드 설정, 번들 예산, 전역 스타일 성능 1절
steiger.config.ts FSD 계층 규칙 강제 패키지 배치와 참조 규칙 9절
eslint.config.js 코드 규약, 전역 프로바이더 위치 제한, 임포트 제한, 템플릿 접근성 패키지 배치와 참조 규칙 7.5절, 보안 3절, 폼과 검증 1절
.postcssrc.json Tailwind 플러그인 등록 디자인 시스템과 토큰
.gitattributes 줄바꿈 정책, 바이너리 지정 본 문서 3절

설정값을 문서에 옮겨 적지 않습니다. 두 벌이 되면 한쪽이 낡습니다. 문서는 규칙과 근거를 담고, 설정 파일이 그 규칙의 실행 형태를 담습니다.

2. 경로 별칭

// tsconfig.json
{ "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }

baseUrl은 지정하지 않습니다. TypeScript 6에서 폐기 예정으로 표시되어 TS5101 오류로 빌드가 실패합니다. 규칙의 원본은 패키지 배치와 참조 규칙 7.3절입니다.

계층 간 임포트에 상대 경로를 쓰는 것은 ESLint no-restricted-imports가 차단합니다. Steiger는 상대 경로로 작성된 위반도 검출하므로 별칭 규칙의 근거는 검출 가능성이 아니라 파일 이동 내성입니다. 슬라이스를 옮길 때 상대 경로는 전부 깨지지만 별칭은 유지됩니다.

3. 줄바꿈 정책

모든 텍스트 파일은 저장소와 체크아웃 양쪽에서 LF를 사용합니다. .gitattributes* text=auto eol=lf 선언이 개인의 core.autocrlf 설정보다 우선합니다.

선언하지 않으면 Windows 환경에서 체크아웃 시 CRLF로 변환되어 커밋마다 경고가 발생하고, 협업 시 줄바꿈만 다른 차이가 생깁니다.

대상 처리
텍스트 전반 LF
*.bat, *.cmd CRLF. cmd가 CRLF를 기대합니다
이미지·폰트·문서 바이너리 변환 금지 (-text)
package-lock.json linguist-generated=true. 사람이 리뷰하지 않습니다

주의

OpenAPI 생성 타입은 linguist-generated로 표시하지 않습니다

생성물을 커밋하는 이유가 계약 변경이 PR 차이에 드러나는 것이기 때문입니다(API 계약 소비 1.1절). 차이 표시를 접으면 필드가 사라진 것을 병합 전에 발견할 수 없게 되어 그 이유가 무효화됩니다.

4. 디렉터리 골격

src/
├── app/
├── pages/
├── shared/
├── main.ts
├── main.server.ts
├── server.ts
└── index.html

app은 FSD app 계층이자 Angular 루트이며 전역 스타일을 포함합니다. pages는 라우트 단위 화면, shared는 비즈니스 로직이 없는 인프라입니다. 나머지 네 파일은 프레임워크 진입점이며 계층 밖입니다.

featuresentities는 재사용이 확인된 시점에 생성합니다. 빈 폴더를 미리 만들지 않습니다.

전역 스타일은 src/app/styles.css이며 angular.jsonstyles 항목이 이 경로를 가리킵니다. Angular CLI 기본값인 src/styles.css에서 이동한 것으로, FSD가 전역 스타일을 app 계층에 두도록 정하고 있기 때문입니다.

주의

src/styles.css를 만들지 않습니다

Spartan CLI의 스타일 진입점 자동 탐지는 angular.json을 먼저 보지 않습니다. <sourceRoot>/styles.{css,scss,sass,less}가 존재하면 그 파일을 쓰고, 없을 때만 angular.jsonbuild.options.styles로 넘어갑니다. src/styles.css가 남아 있으면 angular.json이 가리키지 않는 파일에 테마가 기록되며 오류 없이 진행됩니다. 생성기를 실행할 때 --stylesEntryPoint=src/app/styles.css를 명시하면 탐지 순서와 무관하게 고정됩니다.

5. 명령

명령 용도
npm start 개발 서버
npm run build 운영 빌드. 번들 예산 초과 시 실패합니다
npm test 단위·컴포넌트 테스트
npm run lint 코드 규약 검사
npm run lint:fsd FSD 계층 규칙 검사
npm run check 위 셋을 순서대로 실행
npm run docs:build docs/의 마크다운을 문서 사이트 생성물로 변환

npm run check를 통과하지 않은 상태로 병합하지 않습니다. CI는 이 명령 하나만 실행하면 됩니다.

docs:buildprestart·prebuild·prelint·pretest가 자동으로 앞세우므로 직접 실행할 일은 드뭅니다. 생성물은 src/shared/markdown/generated/에 놓이며 커밋하지 않습니다. OpenAPI 생성 타입을 커밋하는 이유가 계약 변경을 PR 차이에 드러내는 것인데, 문서 HTML 은 원본 마크다운이 같은 저장소에 있어 그 근거가 적용되지 않습니다. 마크다운 차이를 보면 충분합니다.

5.1 문서의 위치와 주소

문서의 계층은 폴더 구조가 원본입니다. 프론트매터에 적지 않습니다. 두 벌이 되면 한쪽이 낡습니다.

docs/
├── architectures/
│   ├── index.md                        영역 개요
│   ├── <domain>/<stack>/index.md       스택 개요
│   └── <domain>/<stack>/<group>/*.md   스택의 문서
└── posts/
    └── <slug>/
        ├── index.md                    글
        └── assets/                     그 글의 이미지

domainfrontend·backend, groupreferences·decisions입니다. 목록에 없는 이름을 쓰면 docs:build가 실패합니다.

주소는 /architectures/<stack>/<slug>/posts/<slug> 로 조립합니다. domaingroup은 주소에 넣지 않습니다. 스택 이름이 이미 유일하고 계층은 사이드바가 보여주므로, 주소에까지 넣으면 길기만 합니다.

글은 폴더 하나가 글 하나입니다. 이미지를 본문 옆에 두어 자산이 문서와 함께 움직입니다. assets/는 문서 수집 대상이 아니므로 그 안에 마크다운을 넣어도 사이트에 나타나지 않습니다. 글의 슬러그는 폴더명이며 프론트매터에 적지 않습니다.

5.2 글의 프론트매터

글은 표준 문서보다 요구하는 값이 많습니다. 목록 카드가 날짜와 표지를 함께 그리기 때문입니다.

항목 필수 설명
title · description 필수 표준 문서와 같습니다
date 필수 YYYY-MM-DD. 다른 형태면 빌드가 실패합니다
cover 필수 ./assets/ 아래의 상대 경로
published 필수 true가 아니면 사이트에 나가지 않습니다
tags · coverColor · order 선택

5.3 이미지

docs:buildsharp로 폭별 이미지를 만들어 public/posts/에 놓고 srcset을 심습니다. 생성물은 커밋하지 않습니다. 원본이 docs/posts/에 있어 문서 HTML과 같은 근거가 적용됩니다.

항목 처리
320 · 640 · 1360px. 원본보다 큰 폭은 만들지 않습니다
형식 webp(품질 82). 벡터(.svg)와 애니메이션(.gif)은 그대로 복사합니다
크기 속성 원본의 width·height를 심어 이미지 도착 전에 자리를 잡습니다

본 저장소 기준으로 원본 3.9MB가 생성물 0.1MB가 됩니다.

외부 주소를 참조하는 이미지는 변환하지 않고 그대로 둡니다. 크기를 알 수 없어 자리를 예약하지 못하므로, 그 이미지가 도착할 때 본문이 밀립니다.

5.4 프론트매터와 본문의 역할

문서마다 slug·title·description필수로 둡니다. 누락되면 docs:build가 실패합니다. slug는 주소의 마지막 조각이며 파일명이 한글이어도 주소가 ASCII로 유지되게 하는 수단입니다.

제목을 누가 소유하는지가 영역마다 다릅니다.

영역 제목의 소유 본문 # 제목
architectures/ 본문 둡니다. 화면은 본문을 그대로 렌더링합니다
posts/ 프론트매터 두지 않습니다. 화면이 표지·제목·날짜·태그를 헤더로 그립니다

표준 문서에서는 프론트매터의 값을 화면에 다시 표시하지 않습니다. 본문이 이미 제목을 갖고 있어 두 값이 같으면 같은 문장이 두 번 보입니다. 두 값이 반드시 같아야 하는 것도 아닙니다. ADR 은 목록용 title이 짧고 본문 제목이 결정 내용을 담은 긴 문장이며, 이 차이는 의도된 것입니다.

글은 반대입니다. 표지와 날짜, 태그가 프론트매터에만 있어 본문이 그것을 담을 수 없습니다. 제목만 본문에 두면 표지와 날짜는 화면이 그리고 제목은 본문이 그리게 되어 헤더가 두 곳으로 갈립니다.

위치 소비처 규격
프론트매터 title 목록, 사이드바, 글의 헤더 목록에서 스캔 가능한 짧은 이름
프론트매터 description 목록 카드 명사구로 종결
본문 최상위 제목(#) 표준 문서의 화면과 마크다운 원본 문서가 다루는 내용을 서술
본문 첫 문단 표준 문서의 화면과 마크다운 원본 문서의 역할을 정의하는 완결 문장

5.5 강조 블록

규칙이 영역마다 다릅니다. 규격을 정의하는 문서와 생각을 풀어 쓰는 글은 강조의 성격이 다릅니다.

영역 허용 종류 한 절(##)당 개수
architectures/ WARNING·IMPORTANT 1개. 넘기면 빌드가 실패합니다
posts/ NOTE·TIP·IMPORTANT·WARNING·CAUTION 다섯 제한 없음

표준 문서에서 쓰는 둘의 조건은 다음과 같습니다.

문법 화면 라벨 사용 조건
> [!WARNING] 주의 위반 시 데이터 손상·인가 우회·기동 실패로 이어지는 제약
> [!IMPORTANT] 중요 작업 전 반드시 확인해야 하는 선행 조건

표준 문서에서 [!NOTE]·[!TIP]·[!CAUTION] 을 쓰는 것을 금지합니다. 근거 설명과 환경별 예외는 본문 문장이나 표의 열로 흡수합니다. 종류를 늘리면 강조의 희소성이 사라져 정작 치명적인 경고가 묻힙니다.

빌드나 린터가 즉시 차단하는 규칙은 블록으로 두지 않습니다. 위반하면 그 자리에서 실패하므로 놓칠 수 없습니다. 블록은 조용히 실패하는 것을 위한 자리입니다. 지연 청크가 만들어지지 않거나, 스크롤 위치 복원이 동작하지 않거나, 하이드레이션이 어긋나는 경우처럼 오류 없이 결과만 달라지는 항목이 대상입니다.

블록 내부는 굵은 소제목 한 줄을 먼저 두고 빈 인용 줄로 본문과 나눕니다.

> [!WARNING]
> **한 줄로 끝나는 소제목**
>
> 본문을 서술합니다.

한 절(##)에 하나를 원칙으로 합니다. 두 개를 넘기면 그 절의 구성을 재검토합니다.

5.6 디렉터리 구조 표기

계층은 tree 기호(├──·└──·)로 표기합니다. 들여쓰기 폭에 의존하지 않아 계층 관계가 기호로 확정됩니다.

코드 블록 안에서 공백으로 열을 맞추는 것을 금지합니다. 본문 글꼴이 한글을 담지 않아 한글만 다른 글꼴로 떨어지며, 그 폭이 고정폭의 배수와 맞지 않아 정렬이 어긋납니다. 원본 마크다운에서만 가지런하고 화면에서는 깨집니다. 근거는 ADR-0014에 있습니다.

항목 설명은 블록 밖에 둡니다. 셋 이상이면 표로, 둘 이하면 본문 문장으로 서술합니다.

shared/auth/
├── session.ts
├── auth-interceptor.ts
└── index.ts

5.7 문서 간 링크

저장소 안을 가리키는 링크는 문서 파일(.md)만 대상으로 합니다. docs:build가 대상 문서를 찾아 사이트 경로로 바꾸며, 찾지 못하면 실패합니다. 사이트 경로는 파일 위치에서 유도하므로 문서에 직접 적지 않습니다.

디렉터리에 링크를 거는 것을 금지합니다. 마크다운 원본에서는 동작하지만 사이트에는 그 경로가 없어 404가 됩니다. 양쪽에서 동시에 성립하지 않는 표기이므로 references/처럼 코드 표기로 언급만 합니다.

표기 판정
[테스트](테스트.md) 허용. 사이트 경로로 변환됩니다
[ADR-0013](../decisions/0013-ADR-생성문서-HTML-신뢰.md) 허용
[references/](references/) 금지. 빌드가 실패합니다
`references/` 허용. 링크 없이 언급합니다
[fsd.how](https://fsd.how/) 허용. 외부 링크는 검사하지 않습니다

5.8 절 참조

절을 가리킬 때는 3.1절 형태로 씁니다. §3.1은 법률·학술 문서의 표기이며 한국어 기술 문서에서는 쓰지 않습니다.

링크를 손으로 만들지 않습니다. docs:build가 절 번호를 해당 절의 앵커로 연결합니다. 세 가지 형태를 인식합니다.

원본 표기 변환 결과
[개발 환경](개발-환경.md) 7절 링크 뒤의 번호가 그 문서의 해당 절로 이어집니다
[아키텍처 9절](../index.md) 링크 자체에 프래그먼트가 붙습니다
3.1절을 그대로 지킵니다 같은 문서의 해당 절로 이어집니다

번호에 해당하는 절이 없으면 빌드가 실패합니다. 코드 블록과 인라인 코드 안의 절 표기는 변환하지 않습니다.

다른 저장소의 문서를 절 번호로 가리키지 않습니다. 우리 문서에 그 앵커가 없어 검증할 수 없습니다. 절 이름으로 서술합니다.

6. 강제 수단의 적용 범위

문서에 적힌 규칙 중 어느 것이 자동으로 차단되고 어느 것이 코드 리뷰에 남는지는 아키텍처 11절이 원본입니다.

요약하면 계층 임포트 방향·공개 API·크로스임포트·조기 추출·번들 예산은 자동 차단되고, shared의 비즈니스 로직 금지·적응형 두 모드 테스트·정적 생성 경로의 적응형 컴포넌트 금지는 수동 확인 대상입니다.

7. 실측으로 확인한 사항

본 표준의 규칙 중 실제로 검증한 항목입니다.

항목 결과
Steiger가 경로 별칭을 인식하는가 인식합니다. @/pages/... 임포트를 계층으로 해석합니다
Steiger가 위반을 실제로 잡는가 shared → pages 임포트는 fsd/forbidden-imports, 공개 API 우회는 fsd/no-public-api-sidestep으로 차단됩니다
진입점 파일이 미분류로 경고되는가 경고되지 않습니다. 의도를 남기기 위해 ignores에 등재했습니다
배럴이 지연 청크 분리를 방해하는가 방해하지 않습니다. 미사용 재수출은 트리셰이킹됩니다
리졸버가 초기 번들에 포함되는가 포함됩니다. 같은 배럴의 컴포넌트는 지연 청크로 분리됩니다
baseUrl 사용 가능 여부 사용 불가입니다. TypeScript 6에서 TS5101 오류가 발생합니다
Steiger가 상대 경로 위반을 잡는가 잡습니다. 별칭 규칙의 근거는 검출 가능성이 아니라 파일 이동 내성입니다
ESLint 커스텀 규칙이 발화하는가 전역 프로바이더 위치, 계층 간 상대 경로, Injector 임포트, Reactive Forms, XSS 우회, console.log가 모두 차단됩니다
동적 토큰 주입을 구문 규칙으로 잡는가 잡지 못합니다. Map.get 등 정상 호출과 구분되지 않아 Injector 임포트 제한으로 대체했습니다
생성물 수정을 린터로 막는가 막지 못합니다. 린터는 편집을 차단하지 않으므로 CI 재생성 검사로 대체했습니다
Spartan helm이 Signal Forms와 연결되는가 직접 연결됩니다. [formField]·[formRoot] 바인딩이며 compat 계층이 필요 없습니다
지연 로딩이 정적 생성 경로의 첫 표시를 늦추는가 늦추지 않습니다. 사전 렌더된 HTML에 지연 청크의 modulepreload 힌트가 포함되어 초기 번들과 병렬로 받습니다
helm 사본의 업스트림 갱신 수단이 있는가 @spartan-ng/cli:healthcheck --autoFix가 폐기 API를 조정합니다. 시각적 개선만 수동 대상입니다
brain이 오버레이 위치 전략 교체를 허용하는가 허용합니다. BrnOverlaypositionStrategy 입력이 기본 전략보다 우선하며, 앱 전역 기본값은 provideBrnOverlayDefaultOptions()로 지정합니다
brain의 캘린더가 키보드와 ARIA를 제공하는가 제공합니다. 화살표·Home·End·PageUp·PageDown, 로빙 탭인덱스, role="grid", 셀의 aria-label·aria-selected·aria-disabled가 모두 구현되어 있습니다
Spartan CLI가 전역 스타일 경로로 src/styles.css를 가정하는가 가정하지만 조건부입니다. 해당 파일이 없으면 angular.jsonstyles로 넘어가 src/app/styles.css를 찾아냅니다. 4절의 주의를 참조합니다
BreakpointObserver가 서버에서 예외를 발생시키는가 발생시키지 않습니다. MediaMatcher가 비브라우저 환경에서 noopMatchMedia로 대체되어 모든 쿼리에 matches: false를 즉시 방출합니다
비ASCII 경로를 정적 생성할 수 있는가 생성은 되지만 서빙되지 않습니다. 한글 경로의 index.html이 정확히 만들어지고 prerendered-routes.json에도 등재되나 요청 시 404가 반환됩니다. 같은 조건에서 ASCII 경로는 200과 ssg를 반환합니다. 4절의 URL 슬러그 규칙이 여기서 나옵니다
allowedHosts 기본값으로 SSR 서버가 동작하는가 동작하지 않습니다. angular.jsonsecurity.allowedHosts가 빈 배열이면 모든 요청이 400으로 거부됩니다. 정적 생성만 확인하면 드러나지 않고 서버를 실제로 띄울 때 전부 막힙니다
Angular sanitizer가 [innerHTML]id를 유지하는가 제거합니다. 목차의 앵커 대상이 사라지며 오류 없이 이동만 실패합니다. 대응은 ADR-0013에 있습니다
Steiger가 슬라이스 그룹 이름을 제한하는가 제한합니다. 그룹 이름이 shared의 세그먼트 이름과 겹치면 fsd/ambiguous-slice-names가 빌드를 실패시킵니다. 표준 세그먼트뿐 아니라 직접 만든 세그먼트도 대상입니다
배럴이 @defer의 의존성 분리를 방해하는가 방해합니다. 같은 배럴에서 즉시 쓰는 심볼과 @defer 안에서만 쓰는 심볼을 함께 가져오면 배럴 모듈이 정적으로 묶여 지연 청크가 만들어지지 않습니다. 임포트 경로만 컴포넌트별로 나눈 결과 초기 번들이 544kB에서 275kB로 줄었습니다
Tailwind preflight 가 buttontext-align 을 되돌리는가 되돌리지 않습니다. 여백·테두리·글꼴·배경은 초기화하지만 정렬은 브라우저 기본값 center 가 남습니다. 버튼 안에서 폭이 내용만큼인 요소는 증상이 드러나지 않고 w-full 인 요소만 가운데로 가므로, 한 버튼 안에서 줄마다 정렬이 갈립니다
afterNextRender 가 정적 생성 시점에도 실행되는가 실행되지 않습니다. 브라우저 전용이라 그 안에서 화면 구성을 바꾸면 생성된 HTML 에 빠지고 하이드레이션 이후에야 나타납니다. 고정된 자리에 요소가 뒤늦게 끼어드는 형태로 드러나며, 개발 서버에서는 부팅이 빨라 알아채기 어렵습니다
내용에 따라 변하는 높이에 CSS 전환을 걸 수 있는가 걸 수 없습니다. 전환은 계산값이 바뀔 때 발화하는데 자식이 늘고 줄어도 height의 계산값은 auto로 같습니다. interpolate-size: allow-keywords0auto 사이를 잇는 수단이라 이 경우에 해당하지 않습니다. 높이를 전환하려면 ResizeObserver로 내용을 재서 픽셀로 지정해야 하며, 관찰 대상은 지정 대상이 아니라 그 안의 내용이어야 값이 자기 자신을 따라가지 않습니다

8. 규칙의 예외

대상 완화한 규칙 사유
src/server.ts, src/main.server.ts no-console Node 진입점은 stdout 이 표준 로깅 채널입니다. 브라우저 콘솔로의 개인정보 유출이라는 규칙의 근거가 적용되지 않습니다
src/pages/** Steiger excessive-slicing 슬라이스가 하나뿐인 초기 단계에서는 과분할 지적이 유효하지 않습니다. 슬라이스가 셋 이상이 되면 제거합니다
src/shared/api/generated/** ESLint 전체 생성물에 대한 지적은 고칠 수 없으므로 잡음이 됩니다
src/shared/markdown/generated/** ESLint 전체 위와 같습니다. scripts/build-docs.mjs 의 산출물입니다
src/shared/ui/** ESLint 선택자 접두사 helm 은 hlm 접두사가 Spartan 규약입니다. app 으로 바꾸면 재생성 때마다 되돌아옵니다
src/shared/ui/** ESLint Injector 임포트 제한 hlm.tsInjectorrunInInjectionContext 인자이며 규칙이 막으려는 Injector.get 이 아닙니다. 나머지 제한(폼 수단, 계층 간 상대 경로)은 유지합니다
src/shared/ui/** ESLint no-input-rename aria-label 처럼 표준 속성명을 그대로 받으려면 별칭이 필요합니다
src/shared/ui/** Steiger public-api, no-reserved-folder-names helm 의 폴더 구조를 생성기가 정합니다. 상세는 디자인 시스템과 토큰 1.1절에 있습니다
전역 Steiger no-public-api-sidestep 컴포넌트별 경로를 우회로 오판하며 규칙 단위 예외가 없습니다. ESLint no-restricted-imports 패턴이 대체하므로 슬라이스 내부 파일 임포트는 여전히 차단됩니다

예외를 추가할 때는 완화한 규칙의 근거가 왜 그 대상에 적용되지 않는지를 함께 적습니다. 사유 없는 예외는 규칙을 무력화합니다.

9. 미확인 항목

항목 확인할 내용 필요 조건
helm 컴포넌트의 오버레이 입력 전달 복사된 helm이 positionStrategy를 brain으로 전달하는지. 전달하지 않으면 helm 사본을 고칩니다 해당 컴포넌트 추가 후
캘린더 라벨의 한국어화 provideBrnCalendarI18n()으로 대체할 항목의 범위 날짜 선택기 구현 시
@defer와 컨텐츠 프로젝션 ng-content 전달 시 제약 적응형 컴포넌트 구현 시
validateHttp의 요청 취소 값이 빠르게 바뀔 때 이전 요청 취소 여부 폼 구현 시
대기 시간 기본값 --wait-delay 200ms · --wait-min 400ms가 실제 응답 분포에 맞는지 실사용 데이터 확보 후
© 2026 dev.goraebap