메인 콘텐츠로 건너뛰기
프런트 컴포넌트는 Twenty의 UI 내부에서 직접 렌더링되는 React 컴포넌트입니다. 이들은 Remote DOM을 사용하는 격리된 Web Worker에서 실행됩니다 — 코드는 샌드박스 처리되지만 iframe이 아닌 페이지 내에서 네이티브로 렌더링됩니다.

프런트 컴포넌트를 사용할 수 있는 위치

프런트 컴포넌트는 Twenty 내에서 두 위치에 렌더링될 수 있습니다:
  • 사이드 패널 — 비헤드리스 프런트 컴포넌트는 오른쪽 사이드 패널에서 열립니다. 이는 명령 메뉴에서 프런트 컴포넌트를 트리거할 때의 기본 동작입니다.
  • 위젯(대시보드 및 레코드 페이지) — 프런트 컴포넌트를 페이지 레이아웃 내 위젯으로 삽입할 수 있습니다. 대시보드 또는 레코드 페이지 레이아웃을 구성할 때 사용자는 프런트 컴포넌트 위젯을 추가할 수 있습니다.
프런트 컴포넌트만으로는 UI에서 직접 접근할 수 없으므로 표시해야 합니다. 이를 수행하는 두 가지 방법은 다음과 같습니다.
  • 명령 메뉴 항목과 연결 — 명령 메뉴(Cmd+K)에 등록하고, 선택적으로 고정된 빠른 작업으로 등록합니다.
  • 페이지 레이아웃에 위젯으로 포함 — 레코드 상세 페이지 또는 대시보드에 배치합니다.

기본 예제

프런트 컴포넌트가 실제로 동작하는 모습을 가장 빨리 확인하는 방법은 defineCommandMenuItem과 연결하여 페이지 오른쪽 상단에 빠른 작업 버튼으로 표시되게 하는 것입니다.
src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk/define';

const HelloWorld = () => {
  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h1>Hello from my app!</h1>
      <p>This component renders inside Twenty.</p>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
  name: 'hello-world',
  description: 'A simple front component',
  component: HelloWorld,
});
src/command-menu-items/hello-world.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';

export default defineCommandMenuItem({
  universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
  shortLabel: 'Hello',
  label: 'Hello World',
  icon: 'IconBolt',
  isPinned: true,
  availabilityType: 'GLOBAL',
  frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
yarn twenty dev로 동기화한 후(또는 일회성으로 yarn twenty dev --once를 실행한 경우), 페이지 우측 상단에 빠른 작업이 표시됩니다:
우측 상단의 빠른 작업 버튼
클릭하면 컴포넌트를 인라인으로 렌더링합니다.

구성 필드

필드필수설명
universalIdentifier이 컴포넌트의 안정적인 고유 ID
componentReact 컴포넌트 함수
name아니요표시 이름
description아니요컴포넌트가 수행하는 작업에 대한 설명
isHeadless아니요컴포넌트에 보이는 UI가 없다면 true로 설정하세요(아래 참조)

프런트 컴포넌트를 페이지에 배치하기

명령 외에도, 페이지 레이아웃에 위젯으로 추가하여 레코드 페이지에 프런트 컴포넌트를 직접 임베드할 수 있습니다. 자세한 내용은 페이지 레이아웃을 참조하세요.

헤드리스 vs 비헤드리스

프런트 컴포넌트는 isHeadless 옵션으로 제어되는 두 가지 렌더링 모드를 제공합니다: 비헤드리스(기본값) — 컴포넌트가 가시적인 UI를 렌더링합니다. 명령 메뉴에서 트리거되면 사이드 패널에서 열립니다. isHeadlessfalse이거나 생략된 경우의 기본 동작입니다. 헤드리스 (isHeadless: true) — 컴포넌트가 백그라운드에서 보이지 않게 마운트됩니다. 사이드 패널을 열지 않습니다. 헤드리스 컴포넌트는 로직을 실행한 뒤 스스로 언마운트하는 작업에 맞게 설계되었습니다 — 예: 비동기 작업 실행, 페이지로 이동, 확인 모달 표시. 아래에 설명된 SDK Command 컴포넌트와 자연스럽게 짝을 이룹니다.
src/front-components/sync-tracker.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component';
import { useEffect } from 'react';

const SyncTracker = () => {
  const recordId = useRecordId();

  useEffect(() => {
    enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
  }, [recordId]);

  return null;
};

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'sync-tracker',
  description: 'Tracks record views silently',
  isHeadless: true,
  component: SyncTracker,
});
컴포넌트가 null을 반환하기 때문에, Twenty는 해당 컴포넌트에 대한 컨테이너 렌더링을 생략합니다 — 레이아웃에 빈 공간이 생기지 않습니다. 컴포넌트는 여전히 모든 훅과 호스트 통신 API에 접근할 수 있습니다.

SDK Command 컴포넌트

twenty-sdk 패키지는 헤드리스 프런트 컴포넌트를 위해 설계된 네 가지 Command 헬퍼 컴포넌트를 제공합니다. 각 컴포넌트는 마운트 시 동작을 실행하고, 스낵바 알림을 표시하여 오류를 처리하며, 완료되면 프런트 컴포넌트를 자동으로 언마운트합니다. twenty-sdk/command에서 임포트하세요:
  • Commandexecute prop을 통해 비동기 콜백을 실행합니다.
  • CommandLink — 앱 경로로 이동합니다. Props: to, params, queryParams, options.
  • CommandModal — 확인 모달을 엽니다. 사용자가 확인하면 execute 콜백을 실행합니다. Props: title, subtitle, execute, confirmButtonText, confirmButtonAccent.
  • CommandOpenSidePanelPage — 특정 사이드 패널 페이지를 엽니다. Props: page, pageTitle, pageIcon.
Command를 사용해 명령 메뉴에서 동작을 실행하는 헤드리스 프런트 컴포넌트의 전체 예시는 다음과 같습니다:
src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/command';
import { CoreApiClient } from 'twenty-sdk/clients';

const RunAction = () => {
  const execute = async () => {
    const client = new CoreApiClient();

    await client.mutation({
      createTask: {
        __args: { data: { title: 'Created by my app' } },
        id: true,
      },
    });
  };

  return <Command execute={execute} />;
};

export default defineFrontComponent({
  universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
  name: 'run-action',
  description: 'Creates a task from the command menu',
  component: RunAction,
  isHeadless: true,
});
src/command-menu-items/run-action.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';

export default defineCommandMenuItem({
  universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
  label: 'Run my action',
  icon: 'IconPlayerPlay',
  frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
});
그리고 실행 전에 확인을 요청하기 위해 CommandModal을 사용하는 예시는 다음과 같습니다:
src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { CommandModal } from 'twenty-sdk/command';

const DeleteDraft = () => {
  const execute = async () => {
    // perform the deletion
  };

  return (
    <CommandModal
      title="Delete draft?"
      subtitle="This action cannot be undone."
      execute={execute}
      confirmButtonText="Delete"
      confirmButtonAccent="danger"
    />
  );
};

export default defineFrontComponent({
  universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
  name: 'delete-draft',
  description: 'Deletes a draft with confirmation',
  component: DeleteDraft,
  isHeadless: true,
});

로직 함수 호출하기

Front 컴포넌트는 샌드박스된 Web Worker 안에서 브라우저 측에서 실행되고, logic functions는 서버 측에서 실행됩니다. 두 요소 사이에는 프로세스 내에서의 직접 호출이 없습니다. 대신, Front 컴포넌트는 HTTP를 통해 로직 함수에 접근합니다. httpRouteTriggerSettings로 선언된 로직 함수는 ${TWENTY_API_URL}/s\<path>/s/ 엔드포인트 아래에 노출됩니다. 여러분의 Front 컴포넌트는 twenty-client-sdk/restRestApiClient를 사용해 해당 라우트를 호출하며, Twenty가 워커에 주입하는 TWENTY_APP_ACCESS_TOKEN으로 인증합니다. RestApiClient는 바로 이런 용도로 설계되었습니다. 이는 워커 환경에서 TWENTY_API_URLTWENTY_APP_ACCESS_TOKEN을 읽어 Authorization: Bearer 헤더를 추가하고, JSON을 직렬화 및 파싱하며, 토큰이나 URL이 없거나 응답이 2xx가 아닐 경우 RestApiClientError를 발생시켜, 여러분이 각 컴포넌트마다 이러한 보일러플레이트를 다시 구현하지 않아도 되도록 해 줍니다. 헤드리스 Front 컴포넌트는 Command 컴포넌트를 통해 마운트 시점에 호출을 실행한 뒤, 자동으로 언마운트될 수 있습니다:
src/front-components/sync-prs.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/command';
import { RestApiClient } from 'twenty-client-sdk/rest';

const SyncPrs = () => {
  const execute = async () => {
    const client = new RestApiClient();

    await client.post('/s/github/fetch-prs', {
      owner: 'twentyhq',
      repo: 'twenty',
    });
  };

  return <Command execute={execute} />;
};

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'sync-prs',
  description: 'Triggers the fetch-prs logic function',
  isHeadless: true,
  component: SyncPrs,
});
클라이언트에 전달되는 경로는 라우트의 public 경로이며, 로직 함수의 httpRouteTriggerSettings.path 앞에 /s가 접두사로 붙습니다. isAuthRequired: true를 유지하세요. 클라이언트가 컴포넌트용으로 Twenty가 발행한 앱 액세스 토큰을 제공합니다:
src/logic-functions/fetch-prs.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';

const handler = async (event: RoutePayload) => {
  const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string };
  // ...fetch from GitHub and persist records...
  return { ok: true };
};

export default defineLogicFunction({
  universalIdentifier: '...',
  name: 'fetch-prs',
  handler,
  httpRouteTriggerSettings: {
    path: '/github/fetch-prs',
    httpMethod: 'POST',
    isAuthRequired: true,
  },
});
TWENTY_API_URLTWENTY_APP_ACCESS_TOKEN은 자동으로 주입됩니다. 자세한 내용은 Application variables을 참고하세요. 비밀 애플리케이션 변수는 Front 컴포넌트에 절대 노출되지 않으므로, API 키 및 기타 민감한 로직은 Front 컴포넌트가 아니라 로직 함수 안에 유지해야 합니다.

RestApiClient 레퍼런스

RestApiClienttwenty-client-sdk/rest에서 import하세요. 이는 CoreApiClientMetadataApiClient와 동일한 클라이언트 패밀리에 속하지만, GraphQL API 대신 앱의 HTTP 라우트를 대상으로 합니다.
방법설명
get(path, options?)GET 요청을 전송합니다.
post(path, body?, options?)POST 요청을 전송합니다.
put(path, body?, options?)PUT 요청을 전송합니다.
patch(path, body?, options?)PATCH 요청을 전송합니다.
delete(path, options?)DELETE 요청을 전송합니다.
request(method, path, options?)임의의 HTTP 메서드를 사용하는 일반적인 요청입니다.
optionsheaders, query(쿼리 문자열 파라미터의 레코드이며, nullish 값은 건너뜁니다), 그리고 signal을 통한 AbortSignal을 받습니다. FormData가 아닌 객체 body는 자동으로 JSON 직렬화됩니다. 401이 발생하면 클라이언트는 호스트를 통해 한 번 액세스 토큰을 갱신한 뒤 요청을 재시도합니다. 기본 URL과 토큰은 기본적으로 환경에서 자동으로 결정됩니다. 테스트 등에서 필요할 때는 생성자에 override를 전달하세요. 예를 들면 다음과 같습니다:
const client = new RestApiClient({
  baseUrl: 'https://api.example.com',
  token: 'my-token',
});
실패한 요청은 status, statusText, url, 파싱된 body를 노출하는 RestApiClientError를 throw합니다:
import { RestApiClient, RestApiClientError } from 'twenty-client-sdk/rest';

const client = new RestApiClient();

try {
  const prs = await client.get('/s/github/fetch-prs', {
    query: { state: 'open' },
  });
} catch (error) {
  if (error instanceof RestApiClientError) {
    console.error(error.status, error.body);
  }
}

런타임 컨텍스트에 접근하기

컴포넌트 내부에서, 현재 사용자, 레코드, 컴포넌트 인스턴스에 접근하려면 SDK 훅을 사용하세요:
src/front-components/record-info.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
  useUserId,
  useRecordId,
  useFrontComponentId,
} from 'twenty-sdk/front-component';

const RecordInfo = () => {
  const userId = useUserId();
  const recordId = useRecordId();
  const componentId = useFrontComponentId();

  return (
    <div>
      <p>User: {userId}</p>
      <p>Record: {recordId ?? 'No record context'}</p>
      <p>Component: {componentId}</p>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
  name: 'record-info',
  component: RecordInfo,
});
사용 가능한 훅:
반환값설명
useUserId()string 또는 null현재 사용자의 ID
useSelectedRecordIds()string[]선택된 모든 기록 ID(선택된 기록이 없으면 빈 배열)
useRecordId()string 또는 null사용 중단됨. 대신 useSelectedRecordIds()를 사용하세요
useFrontComponentId()string이 컴포넌트 인스턴스의 ID
useColorScheme()'light' 또는 'dark'호스트 UI의 활성 색 구성표 (System은 이미 결정됨)
useFrontComponentExecutionContext(selector)항목에 따라 다름셀렉터 함수를 사용해 전체 실행 컨텍스트에 접근

애플리케이션 변수

defineApplication()에서 isSecret: false로 정의된 애플리케이션 변수는 getApplicationVariable 유틸리티를 통해 프론트 컴포넌트 안에서 사용할 수 있습니다:
src/front-components/greeting.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getApplicationVariable } from 'twenty-sdk/front-component';

const Greeting = () => {
  const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';

  return <p>Hello, {recipientName}!</p>;
};

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'greeting',
  component: Greeting,
});
비밀 변수(isSecret: true)는 프론트 컴포넌트에 노출되지 않습니다. 이 변수들은 서버 측에서 실행되는 로직 함수에서만 사용할 수 있습니다. 이는 API 키와 같은 민감한 값이 브라우저로 전송되는 것을 방지합니다.
다음 시스템 변수는 항상 process.env를 통해 사용할 수 있습니다:
변수설명
TWENTY_API_URLTwenty API의 기본 URL
TWENTY_APP_ACCESS_TOKEN앱의 역할 범위로 제한된 단기간 유효한 토큰

호스트 통신 API

프런트 컴포넌트는 twenty-sdk의 함수를 사용해 내비게이션, 모달, 알림을 트리거할 수 있습니다:
함수설명
navigate(to, params?, queryParams?, options?)앱 내 페이지로 이동
openSidePanelPage(params)사이드 패널 열기
closeSidePanel()사이드 패널을 닫습니다
openCommandConfirmationModal(params)확인 대화상자 표시
enqueueSnackbar(params)토스트 알림 표시
unmountFrontComponent()컴포넌트를 언마운트합니다
updateProgress(progress)진행률 표시기 업데이트
호스트 API를 사용하여 동작이 완료된 후 스낵바를 표시하고 사이드 패널을 닫는 예시는 다음과 같습니다:
src/front-components/archive-record.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useRecordId } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';

const ArchiveRecord = () => {
  const recordId = useRecordId();

  const handleArchive = async () => {
    const client = new CoreApiClient();

    await client.mutation({
      updateTask: {
        __args: { id: recordId, data: { status: 'ARCHIVED' } },
        id: true,
      },
    });

    await enqueueSnackbar({
      message: 'Record archived',
      variant: 'success',
    });

    await closeSidePanel();
  };

  return (
    <div style={{ padding: '20px' }}>
      <p>Archive this record?</p>
      <button onClick={handleArchive}>Archive</button>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
  name: 'archive-record',
  description: 'Archives the current record',
  component: ArchiveRecord,
});

여러 기록과의 작업

여러 개의 선택된 기록을 처리하려면 useSelectedRecordIds()를 사용하세요. 이는 일괄 작업에 유용합니다:
src/front-components/bulk-export.tsx
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';

const BulkExport = () => {
  const selectedRecordIds = useSelectedRecordIds();

  const handleExport = async () => {
    const client = new CoreApiClient();

    for (const recordId of selectedRecordIds) {
      await client.mutation({
        updateTask: {
          __args: { id: recordId, data: { exported: true } },
          id: true,
        },
      });
    }

    await enqueueSnackbar({
      message: `Exported ${selectedRecordIds.length} records`,
      variant: 'success',
    });

    await closeSidePanel();
  };

  return (
    <div style={{ padding: '20px' }}>
      <p>Export {selectedRecordIds.length} selected record(s)?</p>
      <button onClick={handleExport}>Export</button>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
  name: 'bulk-export',
  description: 'Export selected records',
  component: BulkExport,
  command: {
    universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
    label: 'Bulk Export',
    availabilityType: 'RECORD_SELECTION',
    conditionalAvailabilityExpression: numberOfSelectedRecords > 0,
  },
});

퍼블릭 에셋

프런트 컴포넌트는 getPublicAssetUrl을 사용해 앱의 public/ 디렉터리의 파일에 접근할 수 있습니다:
import { defineFrontComponent } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';

const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'logo',
  component: Logo,
});
자세한 내용은 퍼블릭 에셋 섹션을 참조하세요.

스타일링

프런트 컴포넌트는 여러 스타일링 방식을 지원합니다. 다음과 같은 방식을 사용할 수 있습니다:
  • 인라인 스타일style={{ color: 'red' }}
  • Twenty UI 컴포넌트twenty-sdk/ui에서 임포트(버튼, 태그, 상태, 칩, 아바타 등)
  • Emotion@emotion/react를 사용하는 CSS-in-JS
  • Styled-componentsstyled.div 패턴
  • Tailwind CSS — 유틸리티 클래스
  • React와 호환되는 모든 CSS-in-JS 라이브러리
import { defineFrontComponent } from 'twenty-sdk/define';
import { Button, Tag, Status } from 'twenty-sdk/ui';

const StyledWidget = () => {
  return (
    <div style={{ padding: '16px', display: 'flex', gap: '8px' }}>
      <Button title="Click me" onClick={() => alert('Clicked!')} />
      <Tag text="Active" color="green" />
      <Status color="green" text="Online" />
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456',
  name: 'styled-widget',
  component: StyledWidget,
});