snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
---
title: CLI
description: "`yarn twenty`는 함수를 실행하고, 로그를 스트리밍하며, 앱 설치를 관리하고, 리모트를 전환하기 위한 명령들을 제공합니다."
icon: terminal
---
`yarn twenty` CLI는 앱과 관련된 모든 작업을 위한 인터페이스입니다. 전체 명령어 목록:
| 명령 | 하는 일 | 관련 문서 |
| ----------------------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `dev` | 소스 파일을 감시하고 변경 사항을 실시간으로 동기화합니다. | [빠른 시작](/l/ko/developers/extend/apps/getting-started/quick-start) |
| `플랜` | 변경 사항을 적용하지 않고 메타데이터 변경 내용을 미리 봅니다. | [동기화 및 복구](/l/ko/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan) |
| `적용` | 계획을 표시한 후 메타데이터 변경 사항을 적용합니다. | [동기화 및 복구](/l/ko/developers/extend/apps/operations/sync-and-recovery) |
| `dev:build` | 앱을 컴파일하고 API 클라이언트를 생성합니다 (`--tarball`을 사용하여 `.tgz`로 패키징합니다). | [게시하기](/l/ko/developers/extend/apps/operations/publishing) |
| `dev:typecheck` | TypeScript 타입 검사를 실행합니다. | [테스트](/l/ko/developers/extend/apps/operations/testing) |
| `dev:add` | 새 엔터티를 스캐폴딩합니다. | [스캐폴딩](/l/ko/developers/extend/apps/getting-started/scaffolding) |
| `dev:generate-client` | 타입이 지정된 API 클라이언트를 다시 생성합니다. | 이 페이지 |
| `dev:function:exec` / `dev:function:logs` | 함수를 실행하고 해당 로그를 스트리밍합니다. | 이 페이지 |
| `dev:translations-extract` | 번역 가능한 문자열을 `locales/` 카탈로그로 추출합니다. | [번역](/l/ko/developers/extend/apps/operations/translations) |
| `dev:catalog-sync` | 마켓플레이스 카탈로그 동기화를 트리거합니다. | [게시하기](/l/ko/developers/extend/apps/operations/publishing#how-marketplace-discovery-works) |
| `app:publish` / `app:install` / `app:uninstall` | 릴리스 라이프사이클 | [게시하기](/l/ko/developers/extend/apps/operations/publishing) 및 이 페이지 |
| `docker:*` | 로컬 Twenty 서버 컨테이너를 관리합니다. | [로컬 서버](/l/ko/developers/extend/apps/getting-started/local-server) |
| `remote:*` | 서버 연결을 관리합니다. | 이 페이지 |
모든 명령은 기본 원격 대신 특정 원격을 대상으로 지정할 수 있도록 `-r, --remote \<name>`를 지원합니다.
## 함수 실행(`yarn twenty dev:function:exec`)
HTTP, 크론, 데이터베이스 이벤트로 트리거하지 않고 로직 함수를 수동으로 실행하세요:
```bash filename="Terminal"
# Execute by function name
yarn twenty dev:function:exec -n create-new-post-card
# Execute by universalIdentifier
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
# Pass a JSON payload
yarn twenty dev:function:exec -n create-new-post-card -p '{"name": "Hello"}'
# Execute the install and uninstall hooks
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
yarn twenty dev:function:exec --uninstall
```
## 함수 로그 보기(`yarn twenty dev:function:logs`)
앱의 로직 함수 실행 로그를 스트리밍합니다:
```bash filename="Terminal"
# Stream all function logs
yarn twenty dev:function:logs
# Filter by function name
yarn twenty dev:function:logs -n create-new-post-card
# Filter by universalIdentifier
yarn twenty dev:function:logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
<Note>
이는 Docker 컨테이너 로그를 표시하는 `yarn twenty docker:logs`와는 다릅니다. `yarn twenty dev:function:logs`는 Twenty 서버에서 앱의 함수 실행 로그를 표시합니다.
</Note>
## 타입이 지정된 클라이언트(`yarn twenty dev:generate-client`) 생성하기
앱을 빌드하거나 동기화하지 않고, 활성 원격의 스키마로부터 타입이 지정된 API 클라이언트(`twenty-client-sdk`)를 다시 생성합니다. 이를 사용해 별도 리포지토리에 존재하는 백엔드 서비스처럼, Twenty 인스턴스와 통신하는 어떤 프로젝트에서든 타입이 지정된 클라이언트를 얻을 수 있습니다:
```bash filename="Terminal"
# In your project (no Twenty app definition required)
yarn add twenty-sdk twenty-client-sdk
# Connect to the Twenty instance to generate the client from
yarn twenty remote:add
# Generate the typed client into node_modules/twenty-client-sdk
yarn twenty dev:generate-client
```
그런 다음 코드에서 클라이언트를 임포트하세요:
```typescript
import { CoreApiClient } from 'twenty-client-sdk/core';
```
데이터 모델이 변경될 때마다 생성된 타입을 갱신하려면 명령을 다시 실행하세요.
<Note>
클라이언트는 `node_modules` 내부에서 생성되므로, 코드와 함께 커밋되지 않습니다. 매번 설치 후(`postinstall` 스크립트나 CI 등) `yarn twenty dev:generate-client`를 실행하세요.
</Note>
## 앱 제거(`yarn twenty app:uninstall`)
활성 워크스페이스에서 앱을 제거하세요:
```bash filename="Terminal"
yarn twenty app:uninstall
# Skip the confirmation prompt
yarn twenty app:uninstall --yes
```
## 리모트 관리
**리모트**는 앱이 연결하는 Twenty 서버를 의미합니다. 설정 과정에서 스캐폴더가 자동으로 하나를 생성합니다. 언제든지 리모트를 추가하거나 전환할 수 있습니다.
```bash filename="Terminal"
# Add a new remote (opens a browser for OAuth login)
yarn twenty remote:add
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
yarn twenty remote:add --local
# Add a remote non-interactively (useful for CI)
yarn twenty remote:add --url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
# List all configured remotes
yarn twenty remote:list
# Set the active remote
yarn twenty remote:use <name>
# Check that the active remote's authentication is still valid
yarn twenty remote:status
# Remove a remote
yarn twenty remote:remove <name>
```
자격 증명은 `~/.twenty/config.json`에 저장됩니다.

View File

@@ -0,0 +1,32 @@
---
title: 개요
description: 앱을 빌드하고, 테스트하며, 배포하세요 — CLI 명령, 통합 테스트, CI, 그리고 서버 또는 npm으로의 게시까지 포함됩니다.
icon: rocket
---
\*\*운영 계층(operations layer)\*\*은 앱을 *사용하는 것*이 아니라 앱에 *수행하는* 모든 작업을 의미합니다. 예를 들어 CLI 명령 실행, 실제 Twenty 서버를 대상으로 한 통합 테스트 실행, CI 구성, 그리고 단일 서버에 배포되는 tarball로 또는 마켓플레이스에 등록되는 npm 패키지로 릴리스를 배포하는 작업 등이 여기에 포함됩니다.
```text
develop ─▶ test ─▶ build ─▶ deploy / publish
─────── ──── ───── ─────────────────
yarn yarn yarn yarn twenty app:publish --private (tarball → one server)
twenty test twenty
dev dev:build yarn twenty app:publish (npm → marketplace)
```
## 이 섹션에서 다루는 내용
<CardGroup cols={2}>
<Card title="CLI" icon="terminal" href="/l/ko/developers/extend/apps/operations/cli">
`yarn twenty` 참고 — exec, logs, uninstall, remotes.
</Card>
<Card title="동기화 및 복구" icon="compass" href="/l/ko/developers/extend/apps/operations/sync-and-recovery">
동기화 차분을 읽을 때 어떤 명령을 사용할지와 복구 단계에 대해 다룹니다.
</Card>
<Card title="테스트" icon="flask" href="/l/ko/developers/extend/apps/operations/testing">
Vitest 설정, 통합 테스트, 타입 체크, CI 워크플로.
</Card>
<Card title="게시" icon="upload" href="/l/ko/developers/extend/apps/operations/publishing">
빌드, tarball 배포, npm 게시, 설치.
</Card>
</CardGroup>

View File

@@ -0,0 +1,293 @@
---
title: 게시
icon: upload
description: Twenty 앱을 마켓플레이스에 배포하거나 내부에 배포하세요.
---
## 개요
앱을 [로컬에서 빌드하고 테스트](/l/ko/developers/extend/apps/getting-started/concepts)하면, 배포 경로는 두 가지입니다:
* **타르볼 배포** — 내부 또는 비공개 사용을 위해 특정 Twenty 서버에 앱을 직접 업로드합니다.
* **npm에 게시** — 모든 워크스페이스가 검색하고 설치할 수 있도록 Twenty 마켓플레이스에 앱을 등재합니다.
두 경로는 동일한 **build** 단계에서 시작합니다.
## 앱 빌드
앱을 컴파일하고 배포 준비가 된 `manifest.json`을 생성하려면 빌드 명령을 실행하세요:
```bash filename="Terminal"
yarn twenty dev:build
```
이는 TypeScript 소스를 컴파일하고, 로직 함수와 프런트 컴포넌트를 트랜스파일하며, 모든 항목을 `.twenty/output/`에 기록합니다. 수동 배포 또는 publish 명령을 위한 `.tgz` 패키지도 생성하려면 `--tarball`을 추가하세요.
## 서버에 배포(타르볼)
공개하고 싶지 않은 앱 — 독점 도구, 엔터프라이즈 전용 통합 또는 실험적 빌드 — 의 경우, 타르볼을 Twenty 서버에 직접 배포할 수 있습니다.
### 사전 준비
배포 전에 대상 서버를 가리키도록 구성된 원격이 필요합니다. 원격은 서버 URL과 인증 자격 증명을 로컬의 `~/.twenty/config.json`에 저장합니다.
원격 추가:
```bash filename="Terminal"
yarn twenty remote:add --url https://your-twenty-server.com --as production
```
### 배포
앱을 한 번에 빌드하여 서버에 업로드:
```bash filename="Terminal"
yarn twenty app:publish --private
# To deploy to a specific remote:
# yarn twenty app:publish --private --remote production
```
### 배포된 앱 공유
타르볼 앱은 공개 마켓플레이스에 나열되지 않으므로, 동일한 서버의 다른 워크스페이스는 탐색만으로는 이를 찾을 수 없습니다. 배포된 앱을 공유하려면:
1. **설정 > 애플리케이션 > 등록**으로 이동하여 앱을 엽니다
2. **Distribution** 탭에서 **공유 링크 복사**를 클릭합니다
3. 이 링크를 다른 워크스페이스의 사용자와 공유하세요 — 해당 앱의 설치 페이지로 바로 이동합니다
공유 링크는 서버의 기본 URL(워크스페이스 하위 도메인 없이)을 사용하므로 서버의 모든 워크스페이스에서 동작합니다.
### 버전 관리
이미 배포된 타르볼 앱을 업데이트할 때, 서버는 `package.json`의 `version`이 현재 배포된 버전보다 [semver](https://semver.org) 순서 기준으로 엄격히 더 높아야 합니다. 동일한 버전을 다시 배포하거나 더 낮은 버전을 푸시하는 경우, 타르볼이 저장되기 전에 거부됩니다 — CLI에서 `VERSION_ALREADY_EXISTS` 오류가 표시됩니다.
업데이트를 릴리스하려면:
1. `package.json`의 `version` 필드를 올리세요(예: `1.2.3` → `1.2.4`, `1.3.0`, 또는 `2.0.0`)
2. `yarn twenty app:publish --private`(또는 `yarn twenty app:publish --private --remote production`)를 실행합니다.
3. 앱이 설치되어 있고 해당 앱의 Settings 탭에서 자동 업그레이드를 활성화한 워크스페이스는 백그라운드에서 자동으로 업그레이드되며, 나머지 워크스페이스는 설정에서 사용 가능한 업그레이드를 확인할 수 있습니다.
<Note>
프리릴리스 태그는 예상대로 동작합니다: `1.0.0-rc.1` → `1.0.0-rc.2`로 올리는 것은 허용되며, `1.0.0`과 같은 최종 릴리스는 `1.0.0-rc.5`보다 더 높은 버전으로 올바르게 인식됩니다. `package.json`의 버전은 유효한 semver 문자열이어야 합니다.
</Note>
{/* TODO: add screenshot of the Upgrade button */}
### 서버 버전 호환성
앱이 특정 Twenty 서버 버전에서 도입된 기능(예: v2.3.0에 추가된 OAuth 공급자)을 사용한다면, `package.json`의 `engines.twenty` 필드를 사용하여 앱에 필요한 최소 서버 버전을 선언해야 합니다:
```json filename="package.json"
{
"name": "twenty-my-app",
"version": "1.0.0",
"engines": {
"node": "^24.5.0",
"twenty": ">=2.3.0"
}
}
```
값은 표준 [semver 범위](https://github.com/npm/node-semver#ranges)입니다. 일반적인 패턴:
| 범위 | 의미 |
| ---------------------------------- | -------------------------------------- |
| `>=2.3.0` | 2.3.0 이상인 모든 서버 |
| `>=2.3.0 \<3.0.0` | 2.3.0 이상이지만 다음 메이저 버전 미만 |
| `^2.3.0` | `>=2.3.0 \<3.0.0`과 동일 |
**배포 및 설치 시 동작:**
* `engines.twenty`가 설정되어 있고 대상 서버의 버전이 해당 범위를 충족하지 않으면, 배포(tarball 업로드) 또는 설치가 `SERVER_VERSION_INCOMPATIBLE` 오류와 함께 거부되며 필요한 범위와 실제 서버 버전을 모두 나타내는 메시지가 표시됩니다.
* `engines.twenty`가 **설정되어 있지 않으면**, 앱은 모든 서버 버전에서 허용됩니다(기존 앱과 하위 호환).
* 서버에 `APP_VERSION`이 구성되어 있지 않으면, 검사가 생략됩니다.
<Note>
서버가 최종 검증을 수행합니다 — 타르볼 업로드와 워크스페이스 설치 모두에서 `engines.twenty`를 검증합니다. 타르볼을 별도 경로로 배포하거나 마켓플레이스에서 설치하더라도, 서버는 여전히 호환성을 강제합니다.
</Note>
## 자동화된 CI/CD (스캐폴드된 워크플로)
`create-twenty-app`으로 생성된 앱은 `.github/workflows/` 아래에 기본으로 세 개의 GitHub Actions 워크플로가 포함되어 있습니다. CI는 별도 설정 없이 실행되며, CD는 하나의 시크릿만 필요하고, npm으로의 배포는 한 번만 설정하면 되는 npm trusted-publisher 설정이 필요합니다.
### CI — `ci.yml`
`main`으로의 푸시와 풀 리퀘스트마다 통합 테스트를 자동으로 실행합니다.
**하는 일:**
1. 앱의 소스 코드를 체크아웃합니다.
2. `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` 컴포지트 액션을 사용해 격리된 Twenty 테스트 인스턴스를 생성합니다(CI에서 `yarn twenty docker:start --test`와 동등합니다).
3. Corepack을 활성화하고, `.nvmrc`에 따라 Node.js를 설정하며, `yarn install --immutable`로 의존성을 설치합니다.
4. 생성된 인스턴스에서 `TWENTY_API_URL`과 `TWENTY_API_KEY`를 전달하여 실제 서버와 통신할 수 있도록 `yarn test`를 실행합니다.
**구성 옵션:**
* `TWENTY_VERSION`(환경 변수, 기본값 `latest`) — `ci.yml`에서 이 값을 수정하여 CI에서 사용하는 Twenty 서버 버전을 고정합니다.
* 동시 실행은 `github.ref`로 그룹화되며, 새 푸시가 발생하면 진행 중인 실행을 취소합니다.
시크릿이 필요하지 않습니다 — 테스트 인스턴스는 일시적이며 작업이 진행되는 동안에만 존재합니다.
### CD — `cd.yml`
매번 `main`으로 푸시될 때 구성된 Twenty 서버에 앱을 배포하며, `deploy` 라벨이 적용된 경우 풀 리퀘스트에서도 선택적으로 배포합니다.
**하는 일:**
1. 라벨이 지정된 PR의 경우 PR 헤드, 아니면 푸시된 커밋을 체크아웃합니다.
2. `twentyhq/twenty/.github/actions/deploy-twenty-app@main`를 실행합니다 — CI에서의 `yarn twenty app:publish --private`에 해당합니다.
3. `twentyhq/twenty/.github/actions/install-twenty-app@main`를 실행하여 새로 배포된 버전을 대상 워크스페이스에 설치합니다.
**필수 구성:**
| 설정 | 위치 | 목적 |
| ----------------------- | --------------------------------------------------------- | ------------------------------------------------ |
| `TWENTY_DEPLOY_URL` | `cd.yml`의 `env`(기본값 `http://localhost:3000`) | 배포 대상 Twenty 서버입니다. 처음 사용하기 전에 실제 서버 URL로 변경하세요. |
| `TWENTY_DEPLOY_API_KEY` | GitHub 저장소 **Settings → Secrets and variables → Actions** | 대상 서버에서 배포 권한이 있는 API 키입니다. |
<Note>
기본 `TWENTY_DEPLOY_URL` 값 `http://localhost:3000`은 플레이스홀더이며 — GitHub 호스팅 러너에서는 아무 곳에도 도달하지 않습니다. CD를 활성화하기 전에 서버의 퍼블릭 URL로 업데이트하세요(또는 네트워크에 접근 가능한 셀프 호스티드 러너를 사용하세요).
</Note>
**PR에서 프리뷰 배포 트리거하기:**
풀 리퀘스트에 `deploy` 라벨을 추가하세요. `cd.yml`의 `if:` 가드는 해당 PR의 헤드 커밋을 사용해 그 PR에 대한 잡을 실행하므로, 머지 전에 대상 서버에서 변경 사항을 검증할 수 있습니다.
### 게시 — `publish.yml`
버전 태그(예: `v1.0.0`)를 푸시하거나 Actions 탭에서 워크플로를 수동으로 실행하면, 출처 정보(provenance)와 함께 앱을 npm에 게시합니다.
**하는 일:**
1. 앱을 체크아웃하고 Node.js를 설정한 다음 npm을 업데이트합니다(신뢰할 수 있는 게시 기능을 사용하려면 npm 11.5.1 이상이 필요합니다).
2. `yarn twenty app:publish`를 실행하여 앱을 빌드하고 `.twenty/output`를 npm에 게시합니다. CI에서는 자동으로 `--provenance` 및 `--access public`을 추가하므로, 워크플로에는 플래그가 필요하지 않습니다.
**한 번만 설정:**
npmjs.com에서 패키지를 연 다음 **Settings → Trusted Publisher**로 이동하여 이 저장소를 `publish.yml` 워크플로에 등록하세요([npm trusted publishing 문서](https://docs.npmjs.com/trusted-publishers)를 참고하세요). provenance와 함께 게시하면 어떤 GitHub 저장소가 패키지를 빌드했는지 증명되며, 이는 Twenty 마켓플레이스에서 앱 소유권을 주장하는 방법이기도 합니다.
<Note>
npm은 **공개(public)** 소스 저장소의 provenance만 허용합니다. 비공개 저장소에서 publish를 수행하면, npm은 OIDC provenance 번들을 `E422 ...` 오류와 함께 거부합니다. 지원되지 않는 GitHub Actions 소스 저장소 가시성: "private"`오류입니다. 비공개 저장소에서 publish하려면, publish 단계의`env`에서 `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'`를 설정하여 provenance를 사용하지 않도록(opt out) 해야 합니다(스캐폴딩된 `publish.yml\`에 주석으로 된 힌트가 포함되어 있습니다):
```yaml filename=".github/workflows/publish.yml"
- name: Publish to npm
env:
TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'
run: yarn twenty app:publish
```
</Note>
### 재사용 가능한 액션 고정하기
`ci.yml` 및 `cd.yml` 워크플로는 `@main`의 재사용 가능한 액션을 참조하므로, `twentyhq/twenty` 저장소의 액션 업데이트가 자동으로 반영됩니다. 결정적 빌드를 원한다면, 각 `uses:` 줄에서 `@main`을 커밋 SHA 또는 릴리스 태그로 바꾸세요.
## npm에 게시
npm에 게시하면 Twenty 마켓플레이스에서 앱을 찾을 수 있게 됩니다. 모든 Twenty 워크스페이스는 UI에서 바로 마켓플레이스 앱을 탐색, 설치 및 업그레이드할 수 있습니다.
### 요구 사항
* [npm](https://www.npmjs.com) 계정
* `package.json`의 `keywords` 배열에 있는 `twenty-app` 키워드(수동으로 추가하세요 — `create-twenty-app` 템플릿에 기본적으로 포함되어 있지 않습니다)
```json filename="package.json"
{
"name": "twenty-app-postcard-sender",
"version": "1.0.0",
"keywords": ["twenty-app"]
}
```
### 마켓플레이스 메타데이터
`defineApplication()` 구성은 마켓플레이스에서 앱이 표시되는 방식을 제어하는 선택적 필드를 지원합니다. `public/` 폴더의 이미지를 참조하려면 `logo`와 `galleryImages`를 사용하세요:
```ts src/application-config.ts
export default defineApplication({
universalIdentifier: '...',
displayName: 'My App',
description: 'A great app',
logo: 'public/logo.png',
galleryImages: [
'public/screenshot-1.png',
'public/screenshot-2.png',
],
});
```
마켓플레이스 필드 전체 목록(`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl` 등)은 Building Apps 페이지의 [defineApplication 아코디언](/l/ko/developers/extend/apps/config/application#marketplace-metadata)을 참조하세요.
#### 권장 갤러리 이미지 크기
마켓플레이스는 `galleryImages`를 고정된 `8:5` 컨테이너에 렌더링합니다(예: `1600×1000 px`).
<Note>
어떤 종횡비의 갤러리 이미지든 전체가 표시되며 잘리지 않습니다. 그러나 `8:5`보다 훨씬 세로로 길거나 가로로 좁은 경우 양쪽에 빈 띠가 표시됩니다.
</Note>
#### 이미지 크기 제한
`logo` 및 각 `galleryImages` 파일은 **10 MB**를 초과해서는 안 됩니다. 마켓플레이스가 게시된 에셋을 다시 호스팅할 때 더 큰 파일은 건너뛰므로 표시되지 않습니다.
### 게시
```bash filename="Terminal"
yarn twenty app:publish
```
특정 dist-tag(예: `beta` 또는 `next`)로 게시하려면:
```bash filename="Terminal"
yarn twenty app:publish --tag beta
```
### 마켓플레이스 검색 방식
Twenty 서버는 npm 레지스트리에서 마켓플레이스 카탈로그를 **매시간** 동기화합니다.
기다리지 않고 즉시 동기화를 트리거할 수 있습니다:
```bash filename="Terminal"
yarn twenty dev:catalog-sync
# To target a specific remote:
# yarn twenty dev:catalog-sync --remote production
```
마켓플레이스에 표시되는 메타데이터는 `defineApplication()` 구성에서 가져옵니다. 위의 [Marketplace metadata](#marketplace-metadata)를 참고하세요.
<Note>
앱에서 `defineApplication()`에 `aboutDescription`을 정의하지 않으면, 마켓플레이스는 소개 페이지 콘텐츠로 npm에 게시된 패키지의 `README.md`를 자동으로 사용합니다. 즉, npm과 Twenty 마켓플레이스 모두에서 하나의 README만 관리하면 됩니다. 마켓플레이스에서 다른 설명을 사용하려면 `aboutDescription`을 명시적으로 설정하세요.
</Note>
### CI 게시
위에서 설명한 스캐폴딩된 `publish.yml` 워크플로는 버전 태그에 따라 provenance와 함께 npm에 자동으로 게시합니다. `yarn twenty app:publish`는 CI에서 실행될 때 `--provenance` 및 `--access public`을 자동으로 추가하므로, 워크플로에는 npm 플래그가 전혀 필요하지 않고 Trusted Publisher를 한 번만 설정하면 됩니다.
다른 CI 시스템(GitLab CI, CircleCI 등)에서는 `yarn install`을 실행한 다음 `yarn twenty app:publish`를 실행하세요. 환경에서 OIDC 토큰을 생성할 수 있을 때는 provenance가 생성되고, 그렇지 않은 경우에는 자동으로 건너뜁니다.
<Note>
**npm provenance**는 npm 목록에 신뢰 배지를 추가하여, 사용자가 공개 CI 파이프라인의 특정 커밋에서 패키지가 빌드되었는지 검증할 수 있게 해줍니다. 또한 Twenty 마켓플레이스에서 앱 소유권을 주장할 수 있게 해 주는 요소이기도 합니다. 자세한 내용은 [npm provenance 문서](https://docs.npmjs.com/generating-provenance-statements)를 참고하세요.
</Note>
## 앱 설치
앱이 게시(npm)되었거나 배포(타르볼)되면, 워크스페이스는 UI를 통해 이를 설치할 수 있습니다.
Twenty UI의 **설정 > 애플리케이션** 페이지로 이동하면 마켓플레이스 앱과 타르볼로 배포된 앱을 모두 탐색하고 설치할 수 있습니다.
{/* TODO: add screenshot of the UI when the app is registered */}
명령줄에서 앱을 설치할 수도 있습니다:
```bash filename="Terminal"
yarn twenty app:install
```
<Note>
서버는 설치 시 semver 버전 규칙을 강제하며, 배포와 동일한 규칙을 적용합니다:
* 워크스페이스에 이미 설치된 것과 동일한 버전을 설치하려고 하면 `APP_ALREADY_INSTALLED` 오류와 함께 거부됩니다.
* 현재 설치된 것보다 낮은 버전을 설치하려고 하면 `CANNOT_DOWNGRADE_APPLICATION` 오류와 함께 거부됩니다.
더 최신 버전을 설치하려면 먼저 배포하거나 게시한 다음 `yarn twenty app:install`을 다시 실행하세요.
</Note>

View File

@@ -0,0 +1,126 @@
---
title: 동기화 및 복구
description: 언제 어떤 명령을 사용할지, 동기화 출력 결과를 읽는 방법, 로컬 메타데이터가 완전 초기화에 이르기 전에 드리프트할 때를 위한 복구 단계별 절차를 다룹니다.
icon: compass
---
로컬 앱 개발은 **동기화**를 중심으로 이루어집니다. CLI는 매니페스트를 다시 빌드하고, 서버는 워크스페이스에 이미 있는 메타데이터와의 차이만 적용합니다. 이 페이지에서는 어떤 명령을 사용할지, 동기화로 무엇이 변경되었는지 읽는 방법, 그리고 로컬 상태가 일관되지 않아 보일 때 순서대로 무엇을 해야 하는지를 설명합니다.
## 언제 어떤 명령을 사용할지
<Note>
일상적인 로컬 반복 개발에서는 거의 항상 `yarn twenty dev`를 사용하면 됩니다. 배포와 게시(publish)는 릴리스를 배포할 때 사용하는 것이며, 로컬 개발 루프용이 **아닙니다**.
</Note>
| 원하는 작업… | 명령 | 노트 |
| ---------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| 라이브 동기화로 로컬에서 반복 개발 | `yarn twenty dev` | 파일을 감시하고 변경될 때마다 동기화합니다. |
| 한 번만 동기화하고 종료 (CI, 스크립트, 훅) | `yarn twenty apply` | 한 번 빌드 + 동기화한 뒤 종료합니다. 파괴적인 변경 확인을 건너뛰려면 `--force`를 추가하세요. |
| 변경 사항을 **실제로 적용하지 않고** 미리 보기 | `yarn twenty plan` | 차이를 계산해 출력만 하고, 아무것도 기록하지 않습니다. |
| 워크스페이스에서 앱 제거 | `yarn twenty app:uninstall` | 프롬프트를 건너뛰려면 `--yes`를 추가하세요. |
| 타르볼을 서버로 전송 | `yarn twenty app:publish --private` | `package.json`의 버전이 **엄격하게 더 높아야** 합니다. 자세한 내용은 [Publishing](/l/ko/developers/extend/apps/operations/publishing)을 참고하세요. |
| 마켓플레이스(npm)에 게시 | `yarn twenty app:publish` | — |
| 배포된 버전 설치 / 업그레이드 | `yarn twenty app:install` | 현재 배포된 버전을 설치합니다. |
| 로컬 서버를 초기화하고 깨끗하게 시작 | `yarn twenty docker:reset` | 로컬 데이터 **전체**를 삭제합니다. 최후의 수단입니다. |
<Note>
`yarn twenty dev --once` 및 `yarn twenty dev --once --dry-run`은 여전히 `yarn twenty apply`와 `yarn twenty plan`의 더 이상 사용되지 않는 별칭으로 작동합니다.
</Note>
### 로컬 동기화에는 버전 증가가 필요 없음
엄격히 증가하는 `version` 규칙(`deploy` 시 `VERSION_ALREADY_EXISTS`, `install` 시 `APP_ALREADY_INSTALLED` / `CANNOT_DOWNGRADE_APPLICATION`)은 **`app:publish` / `app:install`**, 즉 릴리스 경로에만 적용됩니다. `yarn twenty dev`는 매니페스트를 제자리에서 동기화하므로 버전을 변경할 필요가 없습니다. 따라서 반복 개발을 위해 `package.json`을 수정할 필요가 없습니다. 로컬 변경을 테스트하기 위해 버전을 올리고 있다면, 개발 루프가 아니라 릴리스 경로를 사용하고 있는 것입니다.
## 동기화 출력 읽기
각 동기화는 적용된 메타데이터 변경 사항(또는 `plan`으로 했을 때는 적용될 변경 사항)을 Terraform 스타일로 출력합니다. 각 엔티티마다 해당 속성이 포함된 하나의 블록이 출력되고, 그 뒤에 요약 한 줄이 이어집니다:
```text filename="Terminal"
# objectMetadata "rocket" will be created
+ icon = "IconRocket"
+ labelSingular = "Rocket"
+ ...
# fieldMetadata "launchedAt" will be updated
~ isNullable = false -> true
Plan: 2 to add, 1 to change, 1 to destroy.
✓ Synced My App (4 files)
```
이 출력이 1차 진단 도구입니다. 어떤 객체, 필드, 레이아웃이 변경되었는지 정확히 알려주므로, UI를 확인하기 전에 동기화가 예상대로 동작했는지 검증할 수 있습니다.
파괴적인 변경(`to destroy`)은 무엇을 삭제하는지와 함께 나열됩니다(예: `objectMetadata "auditNote" — drops the table and all its rows`), 그리고 대화형 확인이 필요하거나, 스크립트에서는 `--force`가 필요합니다.
동기화가 단일 엔티티에서 실패하면, 오류 메시지에 문제의 엔티티와 그 `universalIdentifier`가 함께 표시됩니다. 예를 들면 다음과 같습니다.
```text
Migration action 'create' for 'fieldMetadata' (universalIdentifier: 2020...4337) failed
```
그 식별자를 사용해 매니페스트(필요하다면 워크스페이스)에서 해당 엔티티를 찾아, 어떤 것이 충돌하는지 추측하지 말고 정확히 확인하세요.
## 변경 사항 미리 보기(plan)
`yarn twenty plan`은 매니페스트를 빌드하고, 서버에 마이그레이션 계획을 요청한 뒤, 이를 출력만 합니다. **아무것도 실제로 적용하지 않습니다**. "이 동기화로 무엇이 바뀔까?"에 안전하게 답할 수 있는 방법으로, 실제로 적용하기 전에 확인할 수 있습니다.
```bash filename="Terminal"
yarn twenty plan
```
```text filename="Terminal"
Building manifest...
Computing metadata plan (read-only, nothing will be applied)...
# fieldMetadata "crewCapacity" will be created
+ ...
Plan: 1 to add, 1 to change, 0 to destroy.
✓ Plan complete for My App — no changes were applied
```
플랜:
* **아무것도 기록하지 않습니다**. 메타데이터 마이그레이션, 애플리케이션 레코드 업데이트, 기본 역할/탭 변경, API 클라이언트 생성이 모두 수행되지 않습니다.
* 실제 동기화에서 적용될 **동일한 diff**를 반환하므로, 생성/업데이트/삭제되는 엔티티를 미리 검토할 수 있습니다.
* 위험한 변경 전에, AI가 생성한 변경 사항을 검토할 때, 또는 예기치 않은 변경이 적용되려 하면 실패해야 하는 스크립트에서 유용합니다.
<Note>
plan은 **메타데이터** 변경만 미리 보여 주며, 앱이 최소 한 번 이상 동기화된 상태(워크스페이스가 이 앱을 알고 있는 상태)여야 합니다. 한 번도 동기화된 적이 없는 앱에 대해 dry run을 실행하면, 서버는 앱이 설치되지 않았다고 보고합니다. 먼저 `yarn twenty dev`를 한 번 실행하세요.
</Note>
## 복구 단계별 절차
로컬 메타데이터가 잘못된 것처럼 보일 때는, 아래 순서대로 단계를 진행하면서 문제가 해결되는 즉시 멈추세요. 각 단계는 이전 단계보다 더 많은 영향을 미칩니다.
1. **재동기화.** `yarn twenty apply`를 다시 실행하세요. 동기화는 멱등적이므로, 깨끗한 매니페스트를 다시 실행해도 안전하며 일시적인 오류가 이 방식으로 해결되는 경우가 많습니다.
2. **계획 미리 보기.** `yarn twenty plan`을 실행해, 다음 동기화가 실제로 적용하지 않고 정확히 무엇을 변경하려 하는지 확인하세요.
3. **명시적인 오류 읽기.** 동기화가 실패하면, 메시지에 포함된 메타데이터 타입과 `universalIdentifier`(위 참조)를 확인한 뒤, 매니페스트에서 해당 엔티티를 찾으세요. 충돌은 보통 중복되었거나 재사용된 식별자를 가리킵니다.
4. **삭제 후 재설치.** `yarn twenty app:uninstall`을 실행한 뒤, 다시 동기화합니다(`yarn twenty dev`). 이 방법은 워크스페이스의 나머지 부분은 그대로 둔 채, 앱의 메타데이터를 깨끗한 상태에서 다시 구축합니다.
5. **전체 초기화(최후의 수단).** `yarn twenty docker:reset`을 실행한 뒤, 다시 시드하고 재동기화합니다.
<Warning>
`yarn twenty docker:reset`은 로컬 인스턴스의 **모든** 데이터를 삭제합니다. 모든 워크스페이스, 레코드, 앱이 제거됩니다. 이전 단계들이 모두 실패했을 때에만 사용하세요.
</Warning>
<Note>
메타데이터 오류가 발생했나요? [이슈를 생성](https://github.com/twentyhq/twenty/issues/new/choose)해 주세요. 이때 실패한 마이그레이션 메시지(메타데이터 타입과 `universalIdentifier` 포함), 동기화 시 출력된 `Metadata changes`, 그리고 실행한 명령들을 함께 첨부해 주세요.
</Note>
## 하나의 워크스페이스에서 동시 동기화 피하기
동기화는 메타데이터 마이그레이션을 적용합니다. **동일한 워크스페이스에 대해 동시에** 여러 번 동기화, 배포, 설치 작업을 실행하면(예: 여러 터미널이나 AI 에이전트가 병렬로 반복 실행하는 경우), 마이그레이션이 서로 얽혀 메타데이터가 부분적으로만 적용된 상태로 남을 수 있습니다.
서버는 이를 방지하기 위해 워크스페이스별로 동기화를 직렬화하지만, 그럼에도 중요한 메타데이터 작업은 동시에 실행하지 말고 **단일** 프로세스를 통해 순차적으로 실행하는 것이 좋습니다. 여러 에이전트로 개발을 오케스트레이션한다면, 이들의 sync/deploy/install 호출을 하나의 큐로 모아 한 번에 하나씩만 실행되도록 하세요.
## 실패 유형 구분하기
문제가 발생했을 때, 메타데이터 diff와 명시적인 오류를 통해 실패 지점을 파악할 수 있습니다.
* **매니페스트 빌드 오류** — 동기화 전에 CLI가 실패합니다(`MANIFEST_BUILD_FAILED`, `TYPECHECK_FAILED`). 앱 소스를 수정하세요.
* **동기화 / 마이그레이션 오류** — 빌드는 성공했지만 diff를 적용하는 데 실패하며, 메시지에 엔티티와 `universalIdentifier`가 표시됩니다. 충돌하는 메타데이터를 수정하세요.
* **종속 항목 크기 오류** — 동기화 또는 설치가 실패했습니다. 앱의 프로덕션 `dependencies`가 런타임 레이어로 설치하기에는 너무 커서 실패했습니다 (`LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED`). 런타임 중에 로직 함수에서 실제로 import하지 않는 패키지(UI 라이브러리, 개발 도구 등)는 `devDependencies`로 이동하세요.
* **앱 코드 런타임 오류** — 동기화는 성공했지만 로직 함수나 컴포넌트가 런타임에 올바르게 동작하지 않는 경우입니다. [function logs](/l/ko/developers/extend/apps/operations/cli)를 확인하세요.
* **로컬 인스턴스 상태** — 위의 어느 경우에도 해당하지 않지만 워크스페이스가 여전히 잘못되어 보이는 경우입니다. 복구 사다리를 순서대로 따라가세요.

View File

@@ -0,0 +1,264 @@
---
title: 테스트
description: Vitest 설정, 실제 Twenty 서버를 대상으로 한 통합 테스트, 타입 검사, 그리고 GitHub Actions를 사용하는 CI.
icon: flask
---
SDK는 테스트 코드에서 앱을 빌드, 배포, 설치 및 제거할 수 있는 프로그래매틱 API를 제공합니다. 이것을 [Vitest](https://vitest.dev/) 및 타입이 지정된 API 클라이언트와 함께 사용하면 실제 Twenty 서버를 대상으로 앱이 종단 간으로 동작하는지 검증하는 통합 테스트를 작성할 수 있습니다.
## npm 패키지 사용
앱에서 원하는 npm 패키지를 설치해 사용할 수 있습니다. 로직 함수와 프런트 컴포넌트는 모두 [esbuild](https://esbuild.github.io/)로 번들되며, 그 의존성은 출력물에 인라인되기 때문에 런타임에는 `node_modules`가 필요하지 않습니다.
### 패키지 설치
```bash filename="Terminal"
yarn add axios
```
그런 다음 코드에서 임포트하세요:
```ts src/logic-functions/fetch-data.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import axios from 'axios';
const handler = async (): Promise<any> => {
const { data } = await axios.get('https://api.example.com/data');
return { data };
};
export default defineLogicFunction({
universalIdentifier: '...',
name: 'fetch-data',
description: 'Fetches data from an external API',
timeoutSeconds: 10,
handler,
});
```
프런트 컴포넌트에서도 동일하게 작동합니다:
```tsx src/front-components/chart.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { format } from 'date-fns';
const DateWidget = () => {
return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'date-widget',
component: DateWidget,
});
```
### 번들링 동작 방식
빌드 단계에서는 esbuild를 사용하여 로직 함수와 프런트 컴포넌트마다 단일 자체 포함 파일을 생성합니다. 가져온 패키지는 아래에 나열된 항목을 제외하고 번들에 인라인됩니다.
**로직 함수**는 Node.js 환경에서 실행됩니다. Node 기본 모듈(`fs`, `path`, `crypto`, `http` 등) 을(를) 사용할 수 있으며 설치할 필요가 없습니다.
**프런트 컴포넌트**는 Web Worker에서 실행됩니다. Node 기본 모듈은 **사용할 수 없습니다** — 브라우저 환경에서 동작하는 npm 패키지만 사용할 수 있습니다. 샌드박스는 *부분적인* DOM만 구현하므로, 패키지가 빌드에는 성공하더라도 런타임에 실패할 수 있습니다. 자세한 내용은 [현재 제한 사항](/l/ko/developers/extend/apps/layout/front-components#current-limitations)을 참고하세요.
두 환경 모두에서 `twenty-client-sdk/core`와 `twenty-client-sdk/metadata`가 사전 제공 모듈로 사용 가능합니다 — 이는 번들되지 않고 서버가 런타임에 해석합니다.
프런트 컴포넌트는 또한 [공유 의존성 번들](/l/ko/developers/extend/apps/layout/front-components#sharing-dependencies-across-front-components)을 통해 의존성을 공유할 수 있습니다. 앱의 package.json에 있는 `frontComponentSharedDependencies` 필드에 나열된 패키지는 한 번만 별도의 파일로 빌드되어, 각 컴포넌트에 인라인되는 대신 런타임에 그 파일에서 로드됩니다. 따라서 공유 패키지만 가져오는 컴포넌트는 거의 자체 의존성 코드를 포함하지 않습니다.
## 설정
스캐폴딩된 앱에는 이미 Vitest가 포함되어 있습니다. 수동으로 설정하는 경우, 의존성을 설치하세요:
```bash filename="Terminal"
yarn add -D vitest vite-tsconfig-paths
```
앱 루트에 `vitest.config.ts`를 생성하세요:
```ts vitest.config.ts
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? '<the pre-seeded local dev key>';
// Make env vars available to globalSetup (test.env only applies to workers)
process.env.TWENTY_API_URL = TWENTY_API_URL;
process.env.TWENTY_API_KEY = TWENTY_API_KEY;
export default defineConfig({
plugins: [
tsconfigPaths({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
fileParallelism: false,
include: ['src/**/*.integration-test.ts'],
globalSetup: ['src/__tests__/global-setup.ts'],
env: {
TWENTY_API_URL,
TWENTY_API_KEY,
},
},
});
```
서버에 연결할 수 있는지 확인하고, SDK용 테스트 구성 파일(`~/.twenty/config.test.json`)을 작성한 다음, 테스트 실행 전에 앱을 동기화하는 글로벌 설정 파일을 생성합니다:
```ts src/__tests__/global-setup.ts
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { appDevOnce, appUninstall } from 'twenty-sdk/cli';
const APP_PATH = process.cwd();
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
export async function setup() {
const apiUrl = process.env.TWENTY_API_URL!;
const apiKey = process.env.TWENTY_API_KEY!;
// Verify the server is running
const response = await fetch(`${apiUrl}/healthz`);
if (!response.ok) {
throw new Error(`Twenty server is not reachable at ${apiUrl}.`);
}
// Write the SDK's test config (the CLI reads config.test.json when NODE_ENV=test)
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(
path.join(CONFIG_DIR, 'config.test.json'),
JSON.stringify({
remotes: { local: { apiUrl, apiKey } },
defaultRemote: 'local',
}, null, 2),
);
// Start from a clean slate, then sync the app
await appUninstall({ appPath: APP_PATH }).catch(() => {});
const result = await appDevOnce({ appPath: APP_PATH });
if (!result.success) {
throw new Error(`Dev sync failed: ${result.error?.message}`);
}
}
export async function teardown() {
await appUninstall({ appPath: APP_PATH });
}
```
## 프로그래매틱 SDK API
`twenty-sdk/cli` 서브 경로는 테스트 코드에서 직접 호출할 수 있는 함수를 내보냅니다:
| 함수 | 설명 |
| -------------- | --------------------------------------------- |
| `appBuild` | 앱을 빌드하고 필요하면 타르볼로 패키징 |
| `appDeploy` | 타르볼을 서버로 업로드 |
| `appDevOnce` | 앱을 한 번만 빌드하고 동기화합니다(`yarn twenty apply`와 동일). |
| `appInstall` | 활성 워크스페이스에 앱 설치 |
| `appUninstall` | 활성 워크스페이스에서 앱 제거 |
각 함수는 `success: boolean`과 `data` 또는 `error`를 포함한 결과 객체를 반환합니다.
## 통합 테스트 작성
다음은 앱을 빌드, 배포, 설치한 후 워크스페이스에 표시되는지 검증하는 전체 예제입니다:
```ts src/__tests__/app-install.integration-test.ts
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const APP_PATH = process.cwd();
describe('App installation', () => {
beforeAll(async () => {
const buildResult = await appBuild({
appPath: APP_PATH,
tarball: true,
onProgress: (message: string) => console.log(`[build] ${message}`),
});
if (!buildResult.success) {
throw new Error(`Build failed: ${buildResult.error?.message}`);
}
const deployResult = await appDeploy({
tarballPath: buildResult.data.tarballPath!,
onProgress: (message: string) => console.log(`[deploy] ${message}`),
});
if (!deployResult.success) {
throw new Error(`Deploy failed: ${deployResult.error?.message}`);
}
const installResult = await appInstall({ appPath: APP_PATH });
if (!installResult.success) {
throw new Error(`Install failed: ${installResult.error?.message}`);
}
});
afterAll(async () => {
await appUninstall({ appPath: APP_PATH });
});
it('should find the installed app in the workspace', async () => {
const metadataClient = new MetadataApiClient();
const result = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
universalIdentifier: true,
},
});
const installedApp = result.findManyApplications.find(
(app: { universalIdentifier: string }) =>
app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(installedApp).toBeDefined();
});
});
```
## 테스트 실행
로컬 Twenty 서버가 실행 중인지 확인한 다음, 다음을 실행하세요:
```bash filename="Terminal"
yarn test
```
또는 개발 중에는 워치 모드로 실행하세요:
```bash filename="Terminal"
yarn test:watch
```
## 타입 검사
테스트를 실행하지 않고도 앱에 대해 타입 검사를 수행할 수 있습니다:
```bash filename="Terminal"
yarn twenty dev:typecheck
```
이는 앱의 `tsconfig.json`에 대해 `tsc --noEmit`를 실행하고 모든 타입 오류를 보고합니다. 스캐폴딩된 앱에는 테스트 파일(`tsconfig.spec.json`)도 검사하는 `yarn typecheck` 스크립트가 함께 제공됩니다.
## GitHub Actions로 CI
스캐폴더는 `.github/workflows/ci.yml`에 바로 사용할 수 있는 워크플로를 생성합니다. `main`으로의 모든 푸시와 모든 풀 리퀘스트마다, 러너에서 임시 Twenty 서버를 실행하고(`twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test` 액션을 통해), 그 서버를 가리키도록 설정된 `TWENTY_API_URL` / `TWENTY_API_KEY`와 함께 `yarn lint`, `yarn typecheck`, `yarn test:unit`, `yarn test`를 실행합니다. 시크릿은 필요 없으며, 워크플로 상단의 `TWENTY_VERSION` 환경 변수로 서버 버전을 고정할 수 있습니다.
스캐폴딩된 세 워크플로(`ci.yml`, `cd.yml` 배포 파이프라인, npm 게시용 `publish.yml`)에 대한 전체 단계별 안내는 [Publishing → Automated CI/CD](/l/ko/developers/extend/apps/operations/publishing#automated-cicd-scaffolded-workflows)를 참고하세요.

View File

@@ -0,0 +1,128 @@
---
title: 번역
description: 여러 언어로 앱을 출시하세요 — 단일 로케일 카탈로그에서 매니페스트 레이블과 UI 문자열을 번역하세요.
icon: language
---
Twenty 앱은 **영어**로 작성됩니다. 소스와 매니페스트의 문자열은 영어 원문이며, `en`은 번역의 기준이 되는 소스 로케일입니다. 번역되지 않은 로케일은 `en`을 기본값으로 사용합니다.
앱에는 두 가지 유형의 번역 가능한 텍스트가 있으며, 둘 다 동일한 `locales/` 카탈로그를 통해 처리됩니다.
* **매니페스트 레이블** — 오브젝트와 필드 이름, 뷰 제목, 메뉴 항목 및 앱 메타데이터에 선언된 기타 문자열.
* **Front-component 문자열** — React 프론트 컴포넌트가 렌더링하는 UI 텍스트입니다.
번역 가능한 문자열에 표시를 하고, 이를 로캘별 카탈로그로 추출한 뒤,
그 카탈로그들을 번역하면, 빌드가 현재 사용자에게 맞는 언어를 제공하므로
별도의 추가 작업이 필요 없습니다.
## Front-component 문자열 표시하기
`twenty-sdk/front-component`에서 번역 헬퍼를 import 하세요:
```tsx
import { Trans, t, msg, useTranslate } from 'twenty-sdk/front-component';
const STATUSES = [
{ id: 'draft', label: msg('Draft') },
{ id: 'sent', label: msg('Sent') },
];
const Card = ({ count, name }: { count: number; name: string }) => {
const { t } = useTranslate();
return (
<section>
{/* Static text — reactive to the user's locale */}
<Trans>Loading postcard…</Trans>
{/* Disambiguate identical sources with a context */}
<Trans context="card-title">Untitled</Trans>
{/* Interpolation: pass values explicitly */}
<p>{t('Hi {name}', { name })}</p>
<p>{t('Saved {count} cards', { count })}</p>
{/* Resolve a lazily-declared descriptor */}
<ul>{STATUSES.map((s) => <li key={s.id}>{t(s.label)}</li>)}</ul>
</section>
);
};
```
### 언제 무엇을 사용할지
* **`Trans`** — JSX 안의 정적 텍스트입니다. 보간을 위해 `message` 및 `values` props를 사용하세요
(`Trans message="Hi {name}" values={{ name }}`);
children 안에서 직접 보간하는 것은 정적으로 추출할 수 없습니다.
* **`useTranslate().t`** — 컴포넌트 내부에서 사용하는 동적 문자열입니다. 사용자가 언어를 전환하면
다시 렌더링됩니다. render 내부에서는 이것을 사용하는 것을 권장합니다.
* **`t(...)`** (직접 import) — 이벤트 핸들러, 헬퍼, 모듈 스코프 등
render 내부뿐만 아니라 **어디서든** 사용할 수 있는 즉시 번역 함수입니다.
* **`msg(...)`** — 데이터(상수, 설정)로 선언된 문자열을 위한 지연 평가용 서술자입니다. 나중에 `t(descriptor)`로 이를 해석하세요.
### 컨텍스트
서로 다른 방식으로 번역되는 동일한 소스 문자열을 구분하기 위해 `context`를 전달하세요:
```tsx
t({ message: 'Open', context: 'door' });
t({ message: 'Open', context: 'window' });
<Trans context="card-title">Untitled</Trans>
```
컨텍스트 문자열은 로케일 카탈로그에서 해당 컨텍스트를 키로 하는 그룹으로 나타납니다:
`"\<context>": { "\<message>": "\<translation>" }`. 매니페스트 레이블은 설계상 컨텍스트 정보를 포함합니다. 각 레이블은 자신이 수행하는 메타데이터 역할(`objectMetadata.labelSingular`, `fieldMetadata.label`, `view.name`, …)을 담고 있으므로, 같은 단어라도 객체 이름으로 쓰일 때와 뷰 이름으로 쓰일 때 서로 다르게 번역될 수 있습니다.
## 추출 및 번역
앱 디렉터리에서 extract 명령을 실행하세요:
```bash
twenty dev:translations-extract # collect strings into locales/en.json
twenty dev:translations-extract --locale fr-FR # also scaffold a target locale
```
추출 과정에서는 manifest 레이블과 front-component 소스의 `t()`/`msg()`/`Trans`
문자열을 모두 `locales/\<locale>.json` 파일에 소스 문자열을 키로 하여 수집합니다. 번역을 채워 넣으세요:
```json
// locales/fr-FR.json
{
"Loading postcard…": "Chargement de la carte…",
"Hi {name}": "Bonjour {name}",
"Saved {count} cards": "{count} cartes enregistrées",
"door": {
"Open": "Ouvrir"
},
"objectMetadata.labelSingular": {
"Postcard": "Carte postale"
}
}
```
문맥이 없는 문자열은 평면 엔트리이고, 문맥이 있는 문자열(모든 매니페스트 레이블 포함)은 해당 컨텍스트 키 아래에 중첩됩니다.
`{name}`과 같은 플레이스홀더는 런타임에 치환되므로 — 번역문에도 그대로 유지해야 합니다. 비워 둔 문자열은 소스 텍스트를 그대로 사용합니다.
## 실행 방식
`twenty dev:build`는 카탈로그를 컴파일하고 현재 사용자에게 맞는 언어를 제공합니다.
manifest 레이블은 서버 측에서 처리되고, front-component 카탈로그는 각 컴포넌트 번들에 내장됩니다. 런타임에는 컴포넌트가 실행 컨텍스트(호스트의 현재 언어)에서 로케일을 읽고, 각 문자열을 해당 카탈로그에서 찾아 해석하며, 번역이 없으면 소스 텍스트로 대체합니다. 호스트에서 언어를 전환하면 `Trans`와
`useTranslate().t` 문자열이 실시간으로 다시 렌더링됩니다.
매니페스트 레이블은 모든 동기화 시 — `twenty dev:build`, `twenty apply`
및 연속적인 `twenty dev` 감시 모두에서 — 컴파일되므로, 로캘 파일을 편집하고
동기화하면 새 텍스트를 확인할 수 있습니다. Front 컴포넌트 카탈로그는 빌드 시
각 컴포넌트 번들에 포함되므로, 그중 하나를 변경하려면 여전히
`twenty dev:build`를 다시 실행하고 (재배포해야) 합니다.
`Trans`의 텍스트 children은 여러 줄에 걸쳐 있을 수 있으며 — 공백은 JSX에서와 동일한 방식으로 축약되므로,
둘 다 `Welcome back` 키로 추출됩니다.
```tsx
<Trans>Welcome back</Trans>
<Trans>
Welcome
back
</Trans>
```