# 클라이언트

모든 앱이 같이 따르는 것. 앱마다의 구조 · 명령 · 서버 부르는 방식은 그 앱의 `AGENTS.md`(`clients/apps/<앱>/`).

```
clients/
├── apps/{web,admin,site,lite,mobile,desktop}/   # React · Vue · Next · 순수 TS · Expo · Tauri
├── packages/client/              # @app/client: createClient<서버.paths>() → get · put · query
├── .oxlintrc.json · .oxfmtrc.json  # 모든 앱 · 패키지가 같이 쓴다 (oxlint · oxfmt 가 위로 찾아 올라간다)
└── nginx.conf.template           # web · admin · lite 이미지의 nginx: 정적 파일 + /api → API_URL(gateway)
```

## 서버 부르기

TS 앱은 부를 서버마다 클라이언트를 하나 만든다. 모든 서버의 타입이 열려 있지만 경로마다 생기는 코드는 없어서
API 가 늘어도 번들은 그대로다. 다른 언어 앱은 `contract/openapi/<서버>.json` 에서 클라이언트를 생성한다 ([contracts.md](/reference/contracts/)).

```ts
const api = createClient<notes.paths>({ name: 'notes', base: 'http://127.0.0.1:8003' }) // name · base(gateway) · fetch 는 선택
await api.get('/api/notes') // 경로 · 응답 타입은 notes 서버 문서에서
await api.post('/api/notes', { text }) // 201 본문(만든 메모)을 돌려준다. put 은 (경로, 본문, { params })
await api.delete('/api/notes/{id}', { params: { id } }) // 경로 변수가 있으면 params 가 필수(빼면 타입 오류), 204 는 void
useQuery(api.query('/api/notes')) // TanStack Query (React · Vue). 키 = [name, 경로, params, query]
```

- `name`: 쿼리 키의 첫 칸. 경로가 같은 서버가 둘이어도 캐시가 섞이지 않게. 주소가 아니라 이름이라 서버 렌더링(절대 주소)과 브라우저(상대 경로)의 키가 같다.
- `base`: 같은 출처(프록시)면 생략, 네이티브 앱 · 서버 렌더링은 서버 주소. `fetch`: CORS 없는 fetch 가 필요할 때(Tauri).
- 주소는 gateway 하나다(`servers/gateway`). 같은 출처 프록시 · nginx 는 `/api` 를 `API_URL`(gateway)로 넘기고, gateway 가 경로 접두사로 서버를 고른다.
  네이티브 앱의 `base` 도 gateway 주소다.
- 서버를 둘 이상 부르면 서버마다 클라이언트를 하나씩 만든다(타입이 서버마다라서). admin 은 `createClient<tags.paths>({ name: 'tags' })` · `createClient<activity.paths>({ name: 'activity' })`.

## 서버 데이터

- TanStack Query (`@tanstack/react-query` · `@tanstack/vue-query`). 쿼리 옵션은 손으로 짜지 않고 `api.query()` 를 쓴다.
- 만들기 · 고치기 · 지우기 뒤에는 `invalidateQueries({ queryKey: api.query('/api/notes').queryKey })` 로 목록을 다시 받는다.
- 프레임워크가 없는 앱(lite · desktop)은 `api.get` · `post` · `put` · `delete` 그대로.
- API 응답 문자열은 텍스트로만 렌더한다 (`innerHTML` · `v-html` · `dangerouslySetInnerHTML` 금지, hook 이 막는다).

## 의존성 · 도구

- 버전은 `pnpm-workspace.yaml` 카탈로그 한 곳에서 (`"react": "catalog:"`). Expo 가 정하는 버전은 `catalog:expo`
  (올릴 때 `npx expo install --fix`), TS 의 JS API 가 필요한 도구(admin 의 vue-tsc · site 의 next build)는 `catalog:ts6`. 서버는 카탈로그를 쓰지 않는다.
- `react` · `react-dom` 은 워크스페이스 전체가 **정확히 같은 한 버전**(Expo 가 정한 것)이다. `^` 범위로 두면 pnpm 이 둘을 따로 골라,
  테스트 · 빌드는 통과하고 브라우저에서만 화면이 빈다.
- 모든 패키지에 `lint`(`oxlint && oxfmt --check`) · `format`, 앱에는 `typecheck`. 기본은 TS 7(`tsc -p .`).
  TS 의 JS API 를 쓰는 도구가 있으면 그 앱만 자기 TS 를 둔다(TS 7 에는 아직 안정된 JS API 가 없다).
- 웹 앱(web · admin · site · lite)은 `Dockerfile` 이 있다. 정적 앱은 nginx 가 `/api` 를 gateway 로 넘기고(`nginx.conf.template`), site(Next)는 standalone.
  mobile · desktop 은 네이티브 빌드라 이미지가 없다 ([development.md](/reference/development/) 의 "이미지").
- 새 앱은 [guides/add-app.md](/guides/add-app/).

## UI

색 · 글자 · 간격 · 둥글기 · 컴포넌트와 그 쓰임은 저장소 루트의 [DESIGN.md](/reference/design/) 가 기준이다
([DESIGN.md 형식](https://github.com/google-labs-code/design.md): 앞의 YAML 이 값, 본문이 규칙). 새 값이 필요하면 거기 먼저 더하고,
`pnpm dlx @google/design.md@0.4.0 lint DESIGN.md` 로 확인한다.