# 코딩컨벤션

> `/gen-html`, `/gen-storybook`, `/review` 에서 공통으로 사용하는 프론트엔드 코딩 규칙이다.

## 참조 조건

- 프론트엔드 마크업, 접근성, 인터랙션, 스타일링 규칙을 확인해야 할 때 이 문서를 읽는다.
- 생성하거나 수정하는 코드가 HTML/SCSS 규칙을 직접 따라야 할 때 이 문서를 읽는다.

## 수행 계약

- 프론트엔드 코드를 생성하거나 수정할 때는 이 문서의 시맨틱 마크업, 접근성, 인터랙션, 스타일링 규칙을 실제 코드에 반영한다.
- 예시가 있는 항목은 금지 예시를 피하고 허용 예시 방향으로 구현한다.

## HTML 컨벤션

- html doctype + html lang 사용

```html
<!DOCTYPE html>
<html lang="ko"></html>
```

- 모든 페이지에 title 정의

```html
<head>
  <title>타이틀명</title>
</head>
```

- header, nav, main, footer 등 시멘틱 요소 사용

```html
<!-- ✅ -->
<header>
  <div>...</div>
</header>
```

- `<button>`은 `type="button"` 명시 (form 안에서 의도치 않은 submit 방지)

```html
<!-- ✅ -->
<button type="button">닫기</button>

<!-- ❌ form 안에서 submit 발생 가능 -->
<button>닫기</button>
```

- 클릭만 필요한 요소에 `<a>` 사용 금지 — `<button>` 사용

```html
<!-- ✅ -->
<button type="button" onClick="{handleClick}">더보기</button>

<!-- ❌ href 없는 a 태그 -->
<a onClick="{handleClick}">더보기</a>
```

- 요소 사이에 간격을 br 태그로 사용하기 보단 div, p 태그와 같은 블록레벨 태그 사용

```html
<!-- ❌ -->
안녕하세요.
<br />
<br />
안녕하세요.

<!-- ✅  -->
<p>안녕하세요.</p>
<p>안녕하세요.</p>
```

- 강조의 의미는 b, i 태그 대신 strong, em 태그 사용(스크린 리더기에서 더 강하게 읽어줌)

```html
<!-- ❌ 의미 없음 -->
<b>굵은 글자</b>

<!-- ❌ 의미 없음 -->
<i>이탤릭 글자</i>

<!-- ✅ 강조의 의미를 갖음(놓치면 안 되는 정보, 주의사항 등 중요한 정보) -->
<strong>굵은 글자</strong>

<!-- ✅ 강조의 의미(비교/대조, 늬앙스 강조) -->
<em>이탤릭 글자</em>
```

- 모든 인터랙티브 요소는 키보드로 접근이 되어야 한다.
- 모든 이미지에는 alt 속성 사용, 장식용 이미지는 빈 alt='' 사용

## CSS 컨벤션

- 글로벌 css가 아니면 module.scss를 사용한다.
- 영업지원 프론트엔드 StyleLint에서 권장하는 rule set을 적용한다. (stylelint-config-dsp-frontend)

```javascript
// .stylelint.js
module.exports = {
  extends: ['stylelint-config-dsp-frontend']
};
```

- 기본적으로 class 명 위주로 스타일링한다.

```css
/* ✅ */
.something {
}

/* ⚠️ */
#something {
}

/* ⚠️ */
div {
}
```

- 의미 있고 직관적인 이름을 사용한다.

```css
✅ .product-card {
}

❌ .box {
}

❌ .blue {
}
```

- 클래스는 간결하게, 깊은 네스팅 스타일은 피한다.

```css
✅ .product-card {
}

❌ .div.container ul li a span {
}
```

- variant가 필요한 부분은 -{variant or modifier}를 붙여서 스타일링한다.
- 공통에서는 엄격하게. 나머지는 자유롭게 사용

```css
.{컴포넌트명}-{variant or modifier} {
}

/* 예 */
.something-large {
}
.something-outline {
}
.something-warning {
}
```

- 피그마 상 변수로 참조되고 있는 부분은 design-system-context.scss를 import 하여 사용한다. (하드코딩 금지)

```scss
@use '@dsp-frontend/design-system-context' as *;

.something {
  // 색상
  color: colors(surface-primary);

  // shadow
  box-shadow: shadow(medium);

  // radius
  border-radius: radius-basic(xlarge);

  // 여백(padding 함수는 padding 속성에 대한 값이 아님)
  padding: padding(medium);
  margin: padding(medium);
  gap: padding(medium);

  // 여백(margin 함수는 margin 속성에 대한 값이 아님)
  padding: margin(medium);
  margin: margin(medium);
  gap: margin(medium);

  // 타이포
  @include applay-typography(title1);
}
```

- 반응형 미디어 쿼리는 `@media`를 직접 작성하지 않고, design-system의 `media` 믹스인을 사용한다.

```scss
@use '@dsp-frontend/design-system-context' as *;

.something {
  // mobile 기본 스타일
  padding: 1rem;

  // ✅ tablet 이상
  @include media(tablet) {
    padding: 2rem;
  }

  // ✅ pc 이상
  @include media(pc) {
    padding: 3rem;
  }
}

// ❌ @media 직접 작성
.something {
  @media (min-width: 768px) {
    padding: 2rem;
  }
}
```

- SCSS nesting은 **3단계 이내**로 제한한다. (선택자 복잡도/specificity 증가 방지)

```scss
// ✅ 2단계
.container {
  .header {
    color: colors(text-body_1);
  }
}

// ❌ 4단계 이상
.container {
  .header {
    .title {
      .icon {
        color: colors(text-body_1);
      }
    }
  }
}
```

- className은 반드시 `classnames/bind` 패턴으로 적용한다. 문자열 직접 전달 금지.
- **예외**: 전역 아토믹 CSS 클래스(예: `text-primary`, `text-secondary` 등 `global.css`/`atomic.css` 에 정의된 유틸 클래스)는 문자열 직접 전달 허용.

```typescript
import classNames from 'classnames/bind';
import styles from './something.module.scss';

const cx = classNames.bind(styles);

// ✅
<div className={cx('root', isActive && 'is-active')} />

// ✅ 전역 아토믹 클래스는 문자열 직접 전달 허용
<span className="text-primary">강조</span>

// ❌ 모듈 스타일을 문자열로 전달
<div className={styles.root} />
<div className="root" />
```

- 인라인 스타일은 가급적 사용하지 않는다.

```javascript
<button style={{ color: 'red' }}>...</button>
```

- !important 사용은 의미 그대로 정말 필요한 곳이 아니면 사용하지 않는다. (CSS의 순수한 철학 무시 개념이므로)

```css
.something {
  /* ⚠️ 반드시 필요한 상황인가? 확인할 것. */
  color: blue !important;
}
```

- 기본 단위는 rem, 무조건 고정이여야하는 px

```css
.something {
  /* ✅ root font-size에 따라 가변 */
  width: 2rem;

  /* ❌ root font-size에 따라 가변 */
  width: 32px;

  /* ✅ root font-size와 별개로 고정 */
  border-radius: 8px;
}
```

- 축약 표현 우선으로 스타일링한다.

```css

.something {
  // ✅
  margin: 0 20px;

  // ⚠️ 진짜 필요한 경우에만
  margin-right: 12px;

  // ❌ 여러개 사용할 경우 축약형으로 ❌
  margin-top: 0;
  margin-right: 20px;
}
```

- Mobile First로 스타일링

```css

@use '@dsp-frontend/design-system-context' as *;

.something {
  // ✅ Mobile
  color: tomato;

  // ✅ Tablet
  @media(tablet) {
    color: blue;
  }

  // ✅ PC
  @media(pc) {
    color: yellow;
  }
}



.something {
  // ❌ PC
  @media(pc) {
    color: yellow;
  }

  // ❌ Tablet
  @media(tablet) {
    color: blue;
  }

  // ❌ Mobile
  color: tomato;

}

```

## Typescript 컨벤션

- 영업지원 프론트엔드 ESLint에서 권장하는 rule set을 적용한다. (dsp-frontend-eslint-config-v8)

```javascript
// .eslintrc.js
module.exports = {
  extends: ['dsp-frontend-eslint-config-v8']
};
```

- **[필수]** 모든 함수에는 JSDoc을 작성한다. `@description`, `@param`(있는 경우), `@returns`(있는 경우) 포함.

```typescript
// ✅ 파라미터와 반환값이 있는 함수
/**
 * @description 사용자 이름을 포맷팅하여 반환한다.
 * @param firstName 이름
 * @param lastName 성
 * @returns 포맷팅된 전체 이름
 */
const formatUserName = (firstName: string, lastName: string): string => {
  return `${lastName} ${firstName}`;
}

// ✅ 파라미터만 있는 함수 (@returns 생략)
/**
 * @description 사용자 정보를 서버에 저장한다.
 * @param user 저장할 사용자 정보
 */
const saveUser = async (user: UserInfo): Promise<void> => { ... }

// ✅ 파라미터가 없는 함수 (@param 생략)
/**
 * @description 현재 로그인한 사용자 정보를 반환한다.
 * @returns 로그인한 사용자 정보. 미로그인 시 null
 */
const getCurrentUser = (): UserInfo | null => { ... }
```

- `any` 사용 금지 — `unknown` + 타입 가드로 대체한다.

```typescript
// ✅ unknown + 타입 가드
const parseResponse = (data: unknown): UserInfo => {
  if (!isUserInfo(data)) throw new Error('Invalid data');
  return data;
};

// ❌ any
const parseResponse = (data: any): UserInfo => {
  return data;
};
```

- `React.FC<Props>` 사용 금지 — Props 타입을 직접 명시한다.

```typescript
// ✅ 직접 타입 명시
const UserName = ({ name }: UserNameProps) => {
  return <span>{name}</span>;
};

// ❌ FC 사용
const UserName: React.FC<UserNameProps> = ({ name }) => {
  return <span>{name}</span>;
};
```

- optional chaining (`?.`) 남용 지양 — 실제로 null/undefined일 수 없는 곳에는 사용하지 않는다.

```typescript
// ✅ 타입상 항상 존재하는 값
const name = user.name;

// ❌ 불필요한 optional chaining (user가 항상 존재하는 경우)
const name = user?.name;
```

- type assertion (`as`) 지양 — 타입 가드를 우선 사용한다.

```typescript
// ✅ 타입 가드
if (isButtonElement(element)) {
  element.disabled = true;
}

// ❌ as로 강제 캐스팅
(element as HTMLButtonElement).disabled = true;
```

- 주석은 TypeScript와 관련된 것들은 /\*\* \*/ 로 작성한다. (상세하면 좋음)
- \*\* 특히 타입의 경우 필드가 많아지는 경우 주석이 없으면 난해해짐

```typescript
// ✅ 주석사용
/**
 * 사용자정보
 */
interface IUserInfo {
  /** 사용자 이름 */
  name: string;
}

// ❌ 주석 미사용
interface IUserInfo {
  name: string;
}

/** http 상태 */
type HttpStatus = 'success' | 'error';
```

- export
- 기본 named export 사용 (사유: 자동완성 / 트리세이킹 최적화 / 명확한 구조)
- Framework에서 export default 강제화하는 부분만 사용
- 이슈가 발생시 "import/de-default-export" 린트 룰을 적용할 수 있음

```typescript
// ✅ 함수
export getUserName = () => {...}

// ✅ 컴포넌트
export UserName = () => {...}

// ⚡️ Page 컴포넌트 (framework에서 강제하는 부분)
export default function Page(){...}
```

- import
- import/order ESLint의 권장사항에 따른다.
- 디스트럭처링
- 특별한 조건이 없는 경우 객체와 배열의 디스트럭처링을 사용한다.

```javascript
const user = { name: '김한화', age: 30 };
const numbers = [1, 2, 3, 4, 5];

// ✅
const { name, age } = user;

// ❌
const name = user.name;
const age = user.age;

// ✅
const [first, second] = numbers;

// ❌
const first = numbers[0];
const second = numbers[1];
```

## React Component 컨벤션

### 기본 원칙

- 단일 책임을 완수하는 컴포넌트를 만들 것 (특히, 공통 컴포넌트)
- YAGNI 원칙 (지금 필요한 것만 구현) & 점진적 개선
- 책임이 여러 개면 책임 개수만큼 분리
- props가 거대해지는 것 지양
- 공통 컴포넌트가 아닌 것들은 페이지 단위로 개발 (반복 사용 시 공통화)
- **God 컴포넌트/함수 지양** — 하나의 파일에 모든 로직을 몰아넣지 않는다

```tsx
// ✅ Compound — props가 커질 때 분리
<Alert.Root>
  <Alert.Indicator />
  <Alert.Content>
    <Alert.Title />
    <Alert.Description />
  </Alert.Content>
</Alert.Root>

// ⚠️ 모든 걸 하나에 담음
<Alert />
```

### 함수 선언

- 컴포넌트/함수 모두 화살표 함수 기본 (framework 강제 부분 제외)
- 함수형 컴포넌트만 사용. 클래스형은 내장 컴포넌트 상속 용도로만.

```tsx
// ✅ 컴포넌트
const UserName = () => { return <>...</> }

// ✅ 순수 함수
const getUserName = () => { ... }

// ☑️ Page 컴포넌트 (framework 강제)
export default function Page() { ... }
```

### Props

- 타입을 정의하여 사용한다. 컴포넌트명 + `Props` 접미사.
- 외부 사용이 필요한 타입은 별도의 파일(`.types.ts`)에서 export 한다.

```typescript
/** 사용자 이름 컴포넌트 Props */
interface UserNameProps {
  /** 제출 핸들러 */
  onUserNameSubmit: () => void;
}

const UserName = ({ onUserNameSubmit }: UserNameProps) => { return <>...</> }
```

### 이벤트 핸들러 네이밍

- Props로 전달: `on*` 접두사
- 컴포넌트 내부: `handle*` 접두사

```tsx
// ✅ 내부 핸들러
const handleModalClose = () => { ... };
const handleUserNameClick = () => { ... };

// ✅ Props로 전달
<UserName
  onUserNameClick={handleUserNameClick}
  onModalClose={handleModalClose}
/>
```

### Container / View 분리

- `xxx.tsx` (container) — 상태, 로직, 이벤트 핸들러
- `xxx.view.tsx` (view) — 마크업, className, props 렌더링만

### 조건부 렌더링

```tsx
// ✅ 단순 표시/미표시 — && 사용
{
  isVisible && <Tooltip />;
}

// ✅ 두 가지 분기 — 삼항 사용
{
  isLoggedIn ? <UserMenu /> : <LoginButton />;
}

// ❌ && 에서 falsy 값(0, '') 주의 — boolean으로 변환
{
  items.length > 0 && <ItemList />;
} // ✅
{
  items.length && <ItemList />;
} // ❌ 0이 렌더링됨
```

### key prop

- 리스트 렌더링 시 `index`를 key로 사용하지 않는다. 고유한 식별자를 사용한다.

```tsx
// ✅ 고유 id 사용
{
  users.map((user) => <UserCard key={user.id} user={user} />);
}

// ❌ index 사용
{
  users.map((user, index) => <UserCard key={index} user={user} />);
}
```

### page 컴포넌트

- Page 파일 자체가 복잡해지는 것을 지양

```tsx
import { ContractsPageContainer } from '@/page-container/contracts/contracts-page-container';

export default function Page() {
  return <ContractsPageContainer />;
}
```

### 메모이제이션

- `useMemo`, `useCallback`, `React.memo`는 실제 성능 문제가 있을 때만 사용한다.
- 모든 값/함수에 무조건 감싸는 과도한 메모이제이션을 자제한다.

```tsx
// ❌ 불필요한 메모이제이션
const userName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);
const handleClick = useCallback(() => {
  setIsOpen(true);
}, []);

// ✅ 단순 연산은 그냥 사용
const userName = `${firstName} ${lastName}`;
const handleClick = () => {
  setIsOpen(true);
};
```

### 모달 사용

- 간단한 알림/확인 모달은 `ModalUtils.alert`, `ModalUtils.confirm`을 사용한다.

```typescript
// ✅ 간단한 알림
ModalUtils.alert('저장되었습니다.');

// ✅ 확인/취소
ModalUtils.confirm('삭제하시겠습니까?');
```

---

## 접근성 가이드

### 기본 원칙

- [WCAG 2.1 AA](https://www.w3.org/TR/WCAG21/) 기준을 목표로 한다.
- 모든 인터랙티브 요소는 **키보드로 접근·조작** 가능해야 한다.
- 스크린 리더 사용자가 콘텐츠를 이해할 수 있도록 **시멘틱 HTML**을 우선 사용한다.

---

### 시멘틱 마크업

```html
<!-- ✅ 시멘틱 요소 사용 -->
<nav aria-label="메인 네비게이션">...</nav>
<main>...</main>
<button type="button">닫기</button>

<!-- ❌ div로 대체 -->
<div onClick="{handleClick}">닫기</div>
```

- 클릭 가능한 요소는 `<button>` 또는 `<a>`를 사용한다.
- `<div>`, `<span>`에 onClick만 붙이지 않는다.

---

### 이미지 & 아이콘

```tsx
// ✅ 의미 있는 이미지 — 설명하는 alt
<img src="/profile.png" alt="사용자 프로필 사진" />

// ✅ 장식용 이미지 — 빈 alt
<img src="/divider.png" alt="" />

// ✅ 아이콘 버튼 — aria-label 필수
<button aria-label="닫기">
  <IconSubUiClose />
</button>
```

---

### 폼 요소

- 모든 입력 요소에 `<label>` 또는 `aria-label`을 제공한다.
- 에러/도움말은 `aria-describedby`로 연결한다.

```tsx
// ✅ label 연결
<label htmlFor="user-name">이름</label>
<input id="user-name" type="text" />

// ✅ 에러 메시지 연결
<input id="email" aria-describedby="email-error" aria-invalid={hasError} />
{hasError && <span id="email-error" role="alert">올바른 이메일을 입력하세요.</span>}
```

---

### 키보드 접근성

| 요소            | 기대 동작                                      |
| --------------- | ---------------------------------------------- |
| 버튼, 링크      | `Tab`으로 포커스, `Enter`/`Space`로 실행       |
| 모달            | 열릴 때 포커스 이동, `Esc`로 닫기, 포커스 트랩 |
| 드롭다운/셀렉트 | `Arrow` 키로 항목 이동, `Enter`로 선택         |
| 탭              | `Arrow` 키로 탭 전환                           |

- `tabIndex={0}`은 네이티브 포커스가 없는 요소에만 사용한다.
- `tabIndex={-1}`은 프로그래밍적 포커스 이동 용도로만 사용한다.
- `tabIndex > 0`은 사용하지 않는다.

---

### ARIA 사용 규칙

- **첫 번째 규칙**: 네이티브 HTML 요소로 해결 가능하면 ARIA를 사용하지 않는다.
- `aria-label`, `aria-labelledby`: 시각적 레이블이 없는 요소에 사용
- `aria-live="polite"`: 동적 콘텐츠 변경 알림 (토스트, 로딩 상태 등)

```tsx
// ✅ 네이티브 HTML로 충분하면 ARIA 불필요
<button>저장</button>

// ✅ 네이티브로 표현 불가할 때만 ARIA 사용
<div role="tablist">
  <button role="tab" aria-selected={isActive}>탭 1</button>
</div>

// ❌ 불필요한 ARIA
<button role="button">저장</button>
```

---

### 색상 & 대비

- 텍스트와 배경의 명암비 **4.5:1 이상** (일반 텍스트), **3:1 이상** (큰 텍스트 18px+)
- 색상만으로 정보를 전달하지 않는다 (에러 표시 시 색상 + 아이콘/텍스트 병행)
- `design-system-context`의 토큰을 사용하면 기본적으로 충족됨
