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,329 @@
---
title: 모범 사례
icon: star
---
이 문서는 프론트엔드 작업 시 따라야 할 모범 사례를 설명합니다.
## 상태 관리
React와 Jotai는 코드베이스에서 상태 관리를 처리합니다.
### Jotai 아톰으로 상태 저장하기
상태를 저장하는 데 필요한 만큼의 atom을 만드는 것이 좋은 습관입니다.
<Warning>
프롭스 드릴링에 너무 많은 노력을 기울이기보다는 추가 아톰을 사용하는 것이 좋습니다.
</Warning>
```tsx
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
defaultValue: 'default value',
});
export const MyComponent = () => {
const [myAtom, setMyAtom] = useAtomState(myAtomState);
return (
<div>
<input
value={myAtom}
onChange={(e) => setMyAtom(e.target.value)}
/>
</div>
);
}
```
### 상태 저장에 `useRef`를 사용하지 마십시오
상태 저장에 `useRef`를 사용하지 않도록 주의하십시오.
상태를 저장하려면 `useState`를 사용하거나 `useAtomState`와 함께 Jotai 아톰을 사용해야 합니다.
일부 리렌더링을 방지하기 위해 `useRef`가 필요하다고 느낄 경우 [리렌더링 관리 방법](#managing-re-renders)을 참조하십시오.
## 리렌더링 관리
리액트에서 리렌더링은 관리하기 어려울 수 있습니다.
불필요한 리렌더링을 피하기 위해 따라야 할 몇 가지 규칙이 있습니다.
리렌더링의 원인을 이해하면 항상 리렌더링을 피할 수 있다는 점을 명심하십시오.
### 루트 레벨에서 작업하기
새 기능에서 리렌더링을 피하는 것은 이제 루트 수준에서 문제를 제거함으로써 쉬워졌습니다.
`PageChangeEffect` 사이드카 컴포넌트에는 페이지 변경 시 실행할 모든 로직을 담고 있는 하나의 `useEffect`만 있습니다.
그렇게 하면 리렌더링을 트리거할 수 있는 곳이 오직 한 곳뿐이라는 것을 알 수 있습니다.
### 코드베이스에 `useEffect`를 추가하기 전에 항상 신중히 고려하세요.
리렌더링은 종종 불필요한 `useEffect` 때문에 발생합니다.
`useEffect`가 정말 필요한지, 아니면 로직을 이벤트 핸들러 함수로 옮길 수 있는지 고민해 보아야 합니다.
일반적으로 `handleClick` 또는 `handleChange` 함수로 로직을 옮기는 것은 어렵지 않다는 것을 알게 될 것입니다.
Apollo와 같은 라이브러리에서 `onCompleted`, `onError` 등을 찾을 수 있습니다.
### 형제 컴포넌트를 사용해 `useEffect` 또는 데이터 페칭 로직 추출하기
루트 컴포넌트에 `useEffect`를 추가해야 한다고 느껴진다면, 사이드카 컴포넌트로 추출하는 것을 고려해야 합니다.
Apollo 훅을 사용하여 데이터 페칭 로직에 동일한 규칙을 적용할 수 있습니다.
```tsx
// ❌ Bad, will cause re-renders even if data is not changing,
// because useEffect needs to be re-evaluated
export const PageComponent = () => {
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
setData(someDependency);
}
}, [someDependency]);
return <div>{data}</div>;
};
export const App = () => (
<PageComponent />
);
```
```tsx
// ✅ Good, will not cause re-renders if data is not changing,
// because useEffect is re-evaluated in another sibling component
export const PageComponent = () => {
const [data, setData] = useAtomState(dataState);
return <div>{data}</div>;
};
export const PageData = () => {
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
setData(someDependency);
}
}, [someDependency]);
return <></>;
};
export const App = () => (
<>
<PageData />
<PageComponent />
</>
);
```
### 아톰 가족 상태 및 선택자 사용하기
아톰 가족 상태와 선택자는 리렌더링을 피하기 위한 훌륭한 방법입니다.
항목 목록을 저장해야 할 때 유용합니다.
### `React.memo(MyComponent)`를 사용하지 마십시오
`React.memo()`를 사용하지 마십시오. 이것은 리렌더링의 원인을 해결하지 않으며, 대신 리렌더 체인을 끊어 의도치 않은 동작을 유발하고 코드 리팩토링을 매우 어렵게 만듭니다.
### `useCallback` 또는 `useMemo` 사용 제한
이들은 종종 필요하지 않으며, 성능 향상을 위해 코드 읽기와 유지보수를 어렵게 만듭니다.
## Console.logs
`console.log` 문은 개발 중에 변수 값과 코드 흐름에 대한 실시간 정보를 제공합니다. 그러나 생산 코드에 남아 있으면 여러 문제를 일으킬 수 있습니다.
1. **성능**: 과도한 로깅은 특히 클라이언트 측 애플리케이션의 경우 런타임 성능에 영향을 미칠 수 있습니다.
2. **보안**: 민감한 데이터 로깅은 브라우저 콘솔을 검사하는 누구나 중요한 정보를 노출할 수 있습니다.
3. **깔끔함**: 콘솔을 로그로 가득 채우면 개발자나 도구가 필요한 중요한 경고나 오류를 가릴 수 있습니다.
4. **전문성**: 사용자가 콘솔을 확인하고 수많은 로그를 보면 코드의 품질과 정교함을 의심할 수 있습니다.
생산 환경에 코드를 올리기 전에 모든 `console.logs`를 제거해야 합니다.
## 이름 지정
### 변수 이름 지정
변수 이름은 변수의 목적이나 기능을 정확히 설명해야 합니다.
#### 일반적인 이름의 문제
프로그래밍에서 일반적인 이름은 특수성이 부족하여 모호성을 유발하고 코드 가독성을 감소시킵니다. 이름이 변수나 함수의 목적을 전달하지 않으면 개발자가 코드의 의도를 깊이 조사해야 합니다. 이로 인해 디버깅 시간이 증가하고 오류에 더 많이 노출되며 유지보수와 협업에 어려움이 생길 수 있습니다. 반면에 설명적인 이름을 사용하면 코드가 자가 설명적이 되고 탐색하기 쉬워져 코드 품질과 개발자 생산성을 향상시킵니다.
```tsx
// ❌ Bad, uses a generic name that doesn't communicate its
// purpose or content clearly
const [value, setValue] = useState('');
```
```tsx
// ✅ Good, uses a descriptive name
const [email, setEmail] = useState('');
```
#### 변수 이름에서 피해야 할 단어들
* 더미
### 이벤트 핸들러
이벤트 핸들러 이름은 `handle`로 시작해야 하고, `on`은 컴포넌트 속성의 이벤트 이름 지정에 사용됩니다.
```tsx
// ❌ Bad
const onEmailChange = (val: string) => {
// ...
};
```
```tsx
// ✅ Good
const handleEmailChange = (val: string) => {
// ...
};
```
## 선택적 Props
선택적 프로퍼티에 대한 기본값 전달을 피하십시오.
**예시**
다음과 같은 `EmailField` 컴포넌트를 보십시오:
```tsx
type EmailFieldProps = {
value: string;
disabled?: boolean;
};
const EmailField = ({ value, disabled = false }: EmailFieldProps) => (
<TextInput value={value} disabled={disabled} fullWidth />
);
```
**사용법**
```tsx
// ❌ Bad, passing in the same value as the default value adds no value
const Form = () => <EmailField value="username@email.com" disabled={false} />;
```
```tsx
// ✅ Good, assumes the default value
const Form = () => <EmailField value="username@email.com" />;
```
## 프로퍼티로써 컴포넌트
가능한 한, 인스턴스화되지 않은 컴포넌트를 prop으로 전달하여 자식이 필요한 prop을 스스로 결정할 수 있도록 하세요.
가장 일반적인 예는 아이콘 컴포넌트입니다.
```tsx
const SomeParentComponent = () => <MyComponent Icon={MyIcon} />;
// In MyComponent
const MyComponent = ({ MyIcon }: { MyIcon: IconComponent }) => {
const theme = useTheme();
return (
<div>
<MyIcon size={theme.icon.size.md}>
</div>
)
};
```
React가 컴포넌트를 컴포넌트로 인식하려면, PascalCase를 사용하여 나중에 `<MyIcon>`으로 인스턴스화해야 합니다.
## Prop 드릴링: 최소화하기
React에서 prop 드릴링은 상태 변수 및 그 설정자를 중간 컴포넌트가 사용하지 않더라도 여러 컴포넌트 계층에 걸쳐 전달하는 관행을 의미합니다. 가끔 필요할 수 있지만, 과도한 prop 드릴링은 다음과 같은 문제를 초래할 수 있습니다:
1. **읽기 어려움**: prop이 어디서 시작되었고 어디에서 사용되는지를 추적하는 것이 깊게 중첩된 컴포넌트 구조에서는 복잡해질 수 있습니다.
2. **유지보수의 어려움**: 한 컴포넌트의 prop 구조의 변화는, 해당 prop을 직접 사용하지 않더라도 여러 컴포넌트에서의 조정이 필요할 수 있습니다.
3. **컴포넌트 재사용성 감소**: 많은 prop을 전달하기 위한 컴포넌트는 그 용도가 덜 일반적이게 되며, 다양한 컨텍스트에서 재사용하기 어려워집니다.
과도한 prop 드릴링을 사용하고 있다고 느껴진다면, [상태 관리 최적 관행](#state-management)을 참조하세요.
## 가져오기
가져올 때는, 지정된 별칭을 사용하고 전체나 상대 경로를 지정하지 않도록 하세요.
**별칭 처리**
```js
{
alias: {
"~": path.resolve(__dirname, "src"),
"@": path.resolve(__dirname, "src/modules"),
"@testing": path.resolve(__dirname, "src/testing"),
},
}
```
**사용법**
```tsx
// ❌ Bad, specifies the entire relative path
import {
CatalogDecorator
} from '../../../../../testing/decorators/CatalogDecorator';
import {
ComponentDecorator
} from '../../../../../testing/decorators/ComponentDecorator';
```
```tsx
// ✅ Good, utilises the designated aliases
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from 'twenty-ui/testing';
```
## 스키마 유효성 검사
[Zod](https://github.com/colinhacks/zod)는 타이핑되지 않은 객체를 위한 스키마 유효성 검사기입니다:
```js
const validationSchema = z
.object({
exist: z.boolean(),
email: z
.string()
.email('Email must be a valid email'),
password: z
.string()
.regex(PASSWORD_REGEX, 'Password must contain at least 8 characters'),
})
.required();
type Form = z.infer<typeof validationSchema>;
```
## 파괴적 변경
테스트가 아직 광범위하게 통합되지 않았으므로, 변경 사항이 다른 부분에 문제를 일으키지 않았는지 철저한 수동 테스트를 통해 보장하세요.

View File

@@ -0,0 +1,110 @@
---
title: 폴더 아키텍처
icon: folder-tree
info: 우리의 폴더 아키텍처를 자세히 살펴보기
---
이 가이드에서는 프로젝트 디렉토리 구조의 세부 사항과 그것이 Twenty의 구조화와 유지관리성에 어떻게 기여하는지 살펴봅니다.
이 폴더 아키텍처 관례를 따르면 특정 기능과 관련된 파일을 더 쉽게 찾을 수 있고, 애플리케이션의 확장성과 유지관리성을 보장하기가 쉬워집니다.
```
front
└───modules
│ └───module1
│ │ └───submodule1
│ └───module2
│ └───ui
│ │ └───display
│ │ └───inputs
│ │ │ └───buttons
│ │ └───...
└───pages
└───...
```
## 페이지
애플리케이션 라우트로 정의된 최상위 컴포넌트를 포함합니다. 모듈 폴더에서 더 하위 수준의 컴포넌트를 가져옵니다(자세한 내용은 아래를 참조하세요).
## 모듈
각 모듈은 특정 컴포넌트, 상태 및 운영 로직을 포함하는 기능 또는 기능 그룹을 나타냅니다.
모두 아래 구조를 따라야 합니다. 모듈 안에 모듈을 중첩할 수 있으며(이를 서브모듈이라고 하며), 동일한 규칙이 적용됩니다.
```
module1
└───components
│ └───component1
│ └───component2
└───constants
└───contexts
└───graphql
│ └───fragments
│ └───queries
│ └───mutations
└───hooks
│ └───internal
└───states
│ └───selectors
└───types
└───utils
```
### 컨텍스트
컨텍스트는 각 레벨에서 수동으로 props를 전달하지 않고, 컴포넌트 트리를 통해 데이터를 전달할 수 있는 방법입니다.
자세한 내용은 [React Context](https://react.dev/reference/react#context-hooks)를 참조하세요.
### GraphQL
프래그먼트, 쿼리 및 뮤테이션이 포함됩니다.
자세한 내용은 [GraphQL](https://graphql.org/learn/)을 참조하세요.
* 프래그먼트
프래그먼트는 쿼리의 재사용 가능한 조각으로, 여러 장소에서 사용할 수 있습니다. 프래그먼트를 사용하면 코드 중복을 피하기가 더 쉬워집니다.
자세한 내용은 [GraphQL 프래그먼트](https://graphql.org/learn/queries/#fragments)를 참조하세요.
* 쿼리
자세한 내용은 [GraphQL 쿼리](https://graphql.org/learn/queries/)를 참조하세요.
* 뮤테이션
자세한 내용은 [GraphQL 뮤테이션](https://graphql.org/learn/queries/#mutations)을 참조하세요.
### 훅
자세한 내용은 [훅](https://react.dev/learn/reusing-logic-with-custom-hooks)을 참조하세요.
### 상태
상태 관리 로직이 포함되어 있습니다. [Jotai](https://jotai.org)가 이것을 처리합니다.
* 셀렉터: 파생 atom(`createAtomSelector` 사용)은 다른 atom으로부터 값을 계산하며 자동으로 메모이제이션됩니다.
React의 내장 상태 관리는 구성 요소 내에서의 상태를 여전히 처리합니다.
### 유틸
재사용 가능한 순수 함수를 포함해야 합니다. 그렇지 않으면 `hooks` 폴더에 커스텀 훅을 만들어야 합니다.
## UI
애플리케이션에서 사용되는 모든 재사용 가능한 UI 컴포넌트를 포함합니다.
이 폴더는 `data`, `display`, `feedback`, `input`과 같은 특정 유형의 컴포넌트에 대한 하위 폴더를 포함할 수 있습니다. 각 컴포넌트는 자기 완결적이고 재사용 가능해야 하며, 애플리케이션의 다른 부분에서 사용할 수 있어야 합니다.
`modules` 폴더의 다른 컴포넌트에서 UI 컴포넌트를 분리함으로써 일관된 디자인을 유지하고, 코드베이스의 다른 부분(비즈니스 로직)에 영향을 주지 않고 UI를 변경하기가 더 쉬워집니다.
## 인터페이스 및 종속성
`ui` 폴더를 제외한 모든 모듈에서 다른 모듈 코드를 가져올 수 있습니다. 이렇게 하면 코드를 쉽게 테스트할 수 있습니다.
### 내부
각 부분(훅, 상태, ...) 모듈의 각 부분은 모듈 내에서만 사용하는 `internal` 폴더를 가질 수 있습니다.

View File

@@ -0,0 +1,91 @@
---
title: 프론트엔드 명령어
icon: terminal
---
## 유용한 명령어
### 앱 시작하기
```bash
npx nx start twenty-front
```
### API GraphQL 스키마를 기반으로 GraphQL 스키마 재생성
```bash
npx nx run twenty-front:graphql:generate --configuration=metadata
```
또는
```bash
npx nx run twenty-front:graphql:generate
```
### 린트
```bash
npx nx run twenty-front:lint # pass --fix to fix lint errors
```
## 번역
```bash
npx nx run twenty-front:lingui:extract
npx nx run twenty-front:lingui:compile
```
### 테스트
```bash
npx nx run twenty-front:test # run jest tests
npx nx run twenty-front:storybook:serve:dev # run storybook
npx nx run twenty-front:storybook:test # run tests # (needs yarn storybook:serve:dev to be running)
npx nx run twenty-front:storybook:coverage # (needs yarn storybook:serve:dev to be running)
```
## 기술 스택
프로젝트는 최소한의 보일러플레이트 코드로 깔끔하고 단순한 스택을 가지고 있습니다.
**앱**
* [React](https://react.dev/)
* [Apollo](https://www.apollographql.com/docs/)
* [GraphQL Codegen](https://the-guild.dev/graphql/codegen)
* [Jotai](https://jotai.org/)
* [TypeScript](https://www.typescriptlang.org/)
**테스트**
* [Jest](https://jestjs.io/)
* [Storybook](https://storybook.js.org/)
**도구**
* [Yarn](https://yarnpkg.com/)
* [Craco](https://craco.js.org/docs/)
* [Oxlint](https://oxc.rs/docs/guide/usage/linter.html)
## 아키텍처
### 라우팅
[React Router](https://reactrouter.com/)가 라우팅을 처리합니다.
불필요한 [재렌더링](/l/ko/developers/contribute/capabilities/frontend-development/best-practices-front#managing-re-renders)을 피하기 위해 모든 라우팅 로직은 `PageChangeEffect`의 `useEffect`에 있습니다.
### 상태 관리
[Jotai](https://jotai.org/)가 상태 관리를 처리합니다.
상태 관리에 대한 자세한 정보는 [최고의 관례](/l/ko/developers/contribute/capabilities/frontend-development/best-practices-front#state-management)를 참조하십시오.
## 테스트
[Jest](https://jestjs.io/)는 유닛 테스트 도구로 사용되고 [Storybook](https://storybook.js.org/)은 컴포넌트 테스트에 사용됩니다.
Jest는 주로 유틸리티 함수 테스트에 사용되며, 직접 컴포넌트를 테스트하지는 않습니다.
Storybook은 개별 컴포넌트의 동작을 테스트하고 디자인 시스템을 표시하는 데 사용됩니다.

View File

@@ -0,0 +1,178 @@
---
title: 단축키
---
## 소개
단축키를 들으려면 일반적으로 `onKeyDown` 이벤트 리스너를 사용합니다.
그러나 `twenty-front`에서는 동일한 시점에 장착된 다른 구성 요소에서 사용되는 같은 단축키 간에 충돌이 발생할 수 있습니다.
예를 들어, Enter 키를 듣는 페이지가 있고, Enter 키를 듣는 모달이 있으며, 그 모달 내에서 Enter 키를 듣는 Select 구성요소가 있다면, 모두 동시에 마운트될 때 충돌이 발생할 수 있습니다.
## `useScopedHotkeys` 훅
이 문제를 해결하기 위해, 우리는 충돌 없이 단축키를 들을 수 있게 하는 커스텀 훅을 만들었습니다.
이를 구성 요소에 배치하면 구성 요소가 마운트될 때 및 지정한 **단축키 스코프**가 활성화될 때만 단축키를 듣습니다.
## 실제로 단축키를 감지하려면 어떻게 하나요?
단축키 청취를 설정하는 데 두 가지 단계가 있습니다:
1. 단축키를 들을 [단축키 스코프](#what-is-a-hotkey-scope-)를 설정합니다.
2. 단축키를 듣기 위해 `useScopedHotkeys` 훅을 사용합니다.
기본 페이지에서도 단축키 스코프를 설정해야 합니다. 왼쪽 메뉴나 명령 메뉴와 같은 다른 UI 요소도 단축키를 들을 수 있기 때문입니다.
## 단축키 사용 사례
일반적으로, 두 가지 단축키를 필요로 하는 사용 사례가 있습니다:
1. 페이지 또는 페이지에 마운트된 구성 요소
2. 사용자 작업으로 인해 포커스를 차지하는 모달 형 구성 요소
두 번째 사용 사례는 재귀적으로 발생할 수 있습니다: 예를 들어, 모달의 드롭다운.
### 페이지에서 단축키 듣기
예시:
```tsx
const PageListeningEnter = () => {
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
// 1. Set the hotkey scope in a useEffect
useEffect(() => {
setHotkeyScopeAndMemorizePreviousScope(
ExampleHotkeyScopes.ExampleEnterPage,
);
// Revert to the previous hotkey scope when the component is unmounted
return () => {
goBackToPreviousHotkeyScope();
};
}, [goBackToPreviousHotkeyScope, setHotkeyScopeAndMemorizePreviousScope]);
// 2. Use the useScopedHotkeys hook
useScopedHotkeys(
Key.Enter,
() => {
// Some logic executed on this page when the user presses Enter
// ...
},
ExampleHotkeyScopes.ExampleEnterPage,
);
return <div>My page that listens for Enter</div>;
};
```
### 모달 형 구성 요소에서 단축키 듣기
이 예시에서는 부모에게 모달을 닫으라고 알리기 위해 Escape 키를 듣는 모달 구성 요소를 사용합니다.
여기서 사용자 상호작용은 범위를 변경합니다.
```tsx
const ExamplePageWithModal = () => {
const [showModal, setShowModal] = useState(false);
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const handleOpenModalClick = () => {
// 1. Set the hotkey scope when user opens the modal
setShowModal(true);
setHotkeyScopeAndMemorizePreviousScope(
ExampleHotkeyScopes.ExampleModal,
);
};
const handleModalClose = () => {
// 1. Revert to the previous hotkey scope when the modal is closed
setShowModal(false);
goBackToPreviousHotkeyScope();
};
return <div>
<h1>My page with a modal</h1>
<button onClick={handleOpenModalClick}>Open modal</button>
{showModal && <MyModalComponent onClose={handleModalClose} />}
</div>;
};
```
그러면 모달 구성 요소에서:
```tsx
const MyDropdownComponent = ({ onClose }: { onClose: () => void }) => {
// 2. Use the useScopedHotkeys hook to listen for Escape.
// Note that escape is a common hotkey that could be used by many other components
// So it's important to use a hotkey scope to avoid conflicts
useScopedHotkeys(
Key.Escape,
() => {
onClose()
},
ExampleHotkeyScopes.ExampleModal,
);
return <div>My modal component</div>;
};
```
단순히 마운트/언마운트 시에 `useEffect`를 사용하는 것만으로는 충돌을 피하기에 충분한지 확신이 서지 않을 때는 이 패턴을 사용하는 것이 중요합니다.
그러한 충돌은 디버깅하기 어려우며, `useEffect`를 사용할 때 더 자주 발생할 수 있습니다.
## 단축키 스코프란 무엇인가요?
단축키 스코프는 단축키가 활성 상태인 컨텍스트를 나타내는 문자열입니다. 일반적으로 열거형으로 인코딩됩니다.
단축키 스코프를 변경하면 해당 스코프를 듣고 있는 단축키는 활성화되고 다른 스코프를 듣고 있는 단축키는 비활성화됩니다.
한 번에 하나의 스코프만 설정할 수 있습니다.
예를 들어 각 페이지의 단축키 스코프는 `PageHotkeyScope` 열거형에 정의되어 있습니다:
```tsx
export enum PageHotkeyScope {
Settings = 'settings',
CreateWorkspace = 'create-workspace',
SignInUp = 'sign-in-up',
CreateProfile = 'create-profile',
PlanRequired = 'plan-required',
ShowPage = 'show-page',
PersonShowPage = 'person-show-page',
CompanyShowPage = 'company-show-page',
CompaniesPage = 'companies-page',
PeoplePage = 'people-page',
OpportunitiesPage = 'opportunities-page',
ProfilePage = 'profile-page',
WorkspaceMemberPage = 'workspace-member-page',
TaskPage = 'task-page',
}
```
내부적으로, 현재 선택한 스코프는 애플리케이션 전반에 걸쳐 공유되는 Jotai atom에 저장됩니다:
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
key: 'currentHotkeyScopeState',
defaultValue: INITIAL_HOTKEYS_SCOPE,
});
```
하지만 이 atom은 수동으로 처리해서는 안 됩니다! 다음 섹션에서 사용하는 방법을 배웁니다.
## 내부적으로 어떻게 작동합니까?
[react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) 위에 얇은 래퍼를 만들어 성능을 높이고 불필요한 재랜더링을 방지합니다.
또한 핫키 스코프 상태를 처리하고 애플리케이션 전반에서 사용할 수 있도록 Jotai atom을 만듭니다.

View File

@@ -0,0 +1,8 @@
---
title: Storybook
description: Twenty의 UI 컴포넌트 라이브러리 둘러보기
---
스토리북에서 완전한 컴포넌트 라이브러리와 문서를 확인하세요.
[Storybook 열기 →](https://storybook.twenty.com)

View File

@@ -0,0 +1,291 @@
---
title: 스타일 가이드
icon: paintbrush
---
이 문서에는 코드 작성 시 따라야 할 규칙이 포함되어 있습니다.
여기서 목표는 읽기 쉽고 유지 관리가 쉬운 일관된 코드베이스를 갖는 것입니다.
이를 위해서는 너무 간결하기보다는 약간 더 장황한 것이 낫습니다.
항상 사람들이 코드를 작성하는 것보다 더 자주 읽는다는 점을 염두에 두십시오. 특히 누구나 기여할 수 있는 오픈 소스 프로젝트에서는 더욱 그렇습니다.
여기에 정의되지 않은 많은 규칙이 있지만 린터에 의해 자동으로 확인됩니다.
## 리액트
### 함수형 컴포넌트 사용
항상 TSX 함수형 컴포넌트를 사용하십시오.
기본 `import`를 `const`와 함께 사용하지 마십시오. 읽기 어렵고 코드 자동 완성으로 import하기 어렵습니다.
```tsx
// ❌ Bad, harder to read, harder to import with code completion
const MyComponent = () => {
return <div>Hello World</div>;
};
export default MyComponent;
// ✅ Good, easy to read, easy to import with code completion
export function MyComponent() {
return <div>Hello World</div>;
};
```
### 프로퍼티
props의 유형을 만들고 내보낼 필요가 없으면 `(ComponentName)Props`라고 명명하십시오.
props 구조 분해를 사용하십시오.
```tsx
// ❌ Bad, no type
export const MyComponent = (props) => <div>Hello {props.name}</div>;
// ✅ Good, type
type MyComponentProps = {
name: string;
};
export const MyComponent = ({ name }: MyComponentProps) => <div>Hello {name}</div>;
```
#### prop 유형을 정의하기 위해 `React.FC` 또는 `React.FunctionComponent`를 사용하지 마십시오.
```tsx
/* ❌ - Bad, defines the component type annotations with `FC`
* - With `React.FC`, the component implicitly accepts a `children` prop
* even if it's not defined in the prop type. This might not always be
* desirable, especially if the component doesn't intend to render
* children.
*/
const EmailField: React.FC<{
value: string;
}> = ({ value }) => <TextInput value={value} disabled fullWidth />;
```
```tsx
/* ✅ - Good, a separate type (OwnProps) is explicitly defined for the
* component's props
* - This method doesn't automatically include the children prop. If
* you want to include it, you have to specify it in OwnProps.
*/
type EmailFieldProps = {
value: string;
};
const EmailField = ({ value }: EmailFieldProps) => (
<TextInput value={value} disabled fullWidth />
);
```
#### JSX 요소에서 단일 변수 prop 확산 방지
JSX 요소에서 `{...props}`와 같은 단일 변수 prop 확산을 피하십시오. 이 관행은 구성 요소가 수신되는 props를 명확하게 하지 않아 가독성과 유지 관리가 어려운 코드를 생성하는 경우가 많습니다.
```tsx
/* ❌ - Bad, spreads a single variable prop into the underlying component
*/
const MyComponent = (props: OwnProps) => {
return <OtherComponent {...props} />;
}
```
```tsx
/* ✅ - Good, Explicitly lists all props
* - Enhances readability and maintainability
*/
const MyComponent = ({ prop1, prop2, prop3 }: MyComponentProps) => {
return <OtherComponent {...{ prop1, prop2, prop3 }} />;
};
```
이유:
* 한눈에 코드가 전달하는 props를 명확하게 하여 이해하고 유지하기 쉽습니다.
* 이것은 구성 요소가 각자의 props로 긴밀하게 결합되는 것을 방지하는 데 도움이 됩니다.
* props를 명시적으로 나열하면 Linting 도구에서 잘못된 철자나 사용되지 않는 props를 쉽게 식별할 수 있습니다.
## 자바스크립트
### nullish 병합 연산자 `??` 사용
```tsx
// ❌ Bad, can return 'default' even if value is 0 or ''
const value = process.env.MY_VALUE || 'default';
// ✅ Good, will return 'default' only if value is null or undefined
const value = process.env.MY_VALUE ?? 'default';
```
### 옵셔널 체이닝 `?.` 사용
```tsx
// ❌ Bad
onClick && onClick();
// ✅ Good
onClick?.();
```
## 타입스크립트
### `interface` 대신 `type` 사용
항상 `interface` 대신 `type`을 사용하십시오. 두 개의 기능은 거의 항상 중첩되며, `type`이 더 유연합니다.
```tsx
// ❌ Bad
interface MyInterface {
name: string;
}
// ✅ Good
type MyType = {
name: string;
};
```
### 열거형 대신 문자열 리터럴 사용
[String literals](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types)는 TypeScript에서 열거형과 같은 값을 처리하는 주요 방법입니다. 이들은 Pick과 Omit으로 확장하기 더 쉽고, 특히 코드 자동 완성 기능을 사용할 때 더 나은 개발자 경험을 제공합니다.
타입스크립트가 열거형 사용을 피하도록 권장하는 이유는 [여기](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#enums)에서 확인할 수 있습니다.
```tsx
// ❌ Bad, utilizes an enum
enum Color {
Red = "red",
Green = "green",
Blue = "blue",
}
let color = Color.Red;
```
```tsx
// ✅ Good, utilizes a string literal
let color: "red" | "green" | "blue" = "red";
```
#### GraphQL 및 내부 라이브러리
GraphQL 코드 생성기가 생성한 열거형을 사용해야 합니다.
내부 라이브러리를 사용할 때도 열거형을 사용하는 것이 좋으며, 내부 API와 관련이 없는 문자열 리터럴 유형을 노출할 필요가 없습니다.
예시:
```TSX
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
setHotkeyScopeAndMemorizePreviousScope(
RelationPickerHotkeyScope.RelationPicker,
);
```
## 스타일링
### StyledComponents 사용
[Linaria styled](https://github.com/callstack/linaria)로 구성 요소를 스타일링하십시오.
```tsx
// ❌ Bad
<div className="my-class">Hello World</div>
```
```tsx
// ✅ Good
const StyledTitle = styled.div`
color: red;
`;
```
스타일드 컴포넌트 앞에 "Styled"를 붙여 실제 컴포넌트와 구분하십시오.
```tsx
// ❌ Bad
const Title = styled.div`
color: red;
`;
```
```tsx
// ✅ Good
const StyledTitle = styled.div`
color: red;
`;
```
### 테마
대부분의 구성 요소 스타일링에는 테마를 활용하는 것이 선호되는 접근 방식입니다.
#### 측정 단위
스타일드 컴포넌트 내에서 `px` 또는 `rem` 값을 직접 사용하는 것을 피하십시오. 필요한 값은 일반적으로 테마에 이미 정의되어 있으므로 이러한 목적으로 테마를 활용하는 것이 좋습니다.
#### 색상
새로운 색상을 도입하지 말고 테마에서 기존 팔레트를 사용하십시오. 팔레트가 맞지 않는 경우가 있다면 팀이 수정할 수 있도록 댓글을 남겨야 합니다.
```tsx
// ❌ Bad, directly specifies style values without utilizing the theme
const StyledButton = styled.button`
color: #333333;
font-size: 1rem;
font-weight: 400;
margin-left: 4px;
border-radius: 50px;
`;
```
```tsx
// ✅ Good, utilizes the theme
const StyledButton = styled.button`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.regular};
margin-left: ${({ theme }) => theme.spacing(1)};
border-radius: ${({ theme }) => theme.border.rounded};
`;
```
## No-Type Import 강제 실행
타입 import를 피하십시오. 이 표준을 강제하기 위해 Oxlint 규칙이 모든 타입 import를 검사하고 보고합니다. 이는 TypeScript 코드의 일관성과 가독성을 유지하는 데 도움이 됩니다.
```tsx
// ❌ Bad
import { type Meta, type StoryObj } from '@storybook/react';
// ❌ Bad
import type { Meta, StoryObj } from '@storybook/react';
// ✅ Good
import { Meta, StoryObj } from '@storybook/react';
```
### No-Type Import의 이유
* **일관성**: 타입 import를 피하고, 타입과 값 import 모두의 단일 접근 방식을 사용하여 모듈 import 스타일의 일관성을 유지합니다.
* **가독성**: No-Type Import는 값을 가져오거나 타입을 가져오는 것을 명확히 하여 코드 가독성을 향상시킵니다. 이는 모호성을 줄이고 가져온 기호의 목적을 이해하기 쉽게 만듭니다.
* **유지관리성**: 이는 코드베이스 유지관리를 향상시킵니다. 개발자가 코드를 검토하거나 수정할 때 타입 전용 import를 식별하고 찾을 수 있습니다.
### Oxlint 규칙
Oxlint 규칙인 `typescript/consistent-type-imports`는 타입 import 금지 표준을 강제합니다. 이 규칙은 모든 타입 import 위반에 대해 오류나 경고를 생성합니다.
이 규칙은 의도치 않은 타입 import가 발생하는 드문 경계 사례를 구체적으로 다룹니다. 타입스크립트 자체는 [TypeScript 3.8 릴리스 노트](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html)에서 이 관행을 권장하지 않습니다. 대부분의 상황에서 타입 전용 import를 사용할 필요가 없습니다.
코드가 이 규칙을 준수하도록 개발 워크플로의 일부로 Oxlint를 실행하십시오.

View File

@@ -0,0 +1,60 @@
---
title: Figma와 함께 작업하기
info: Twenty의 Figma와 협업하는 방법을 배우기
---
Figma는 디자이너와 개발자 간의 소통 장벽을 허무는 협업 인터페이스 디자인 도구입니다.
이 가이드는 Figma와 협업하는 방법을 설명합니다.
## 접근하기
1. **공유 링크에 접근:** 프로젝트의 Figma 파일에 [여기서](https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty) 접근할 수 있습니다.
2. **로그인:** 이미 로그인하지 않은 경우, Figma에서 로그인 하라는 요청을 받을 것입니다.
전문 모드 및 전용 프레임 선택 기능과 같이 로그인한 사용자에게만 제공되는 주요 기능이 있습니다.
<Warning>
계정 없이는 효과적으로 협업할 수 없습니다.
</Warning>
## Figma 구조
왼쪽 사이드바에서 Twenty의 Figma의 다양한 페이지에 접근할 수 있습니다. 이들은 이렇게 구성되어 있습니다:
* **구성 요소 페이지:** 첫 번째 페이지입니다. 디자이너는 디자인 파일 전반에 걸쳐 재사용 가능한 디자인 요소를 생성하고 조직하는 데 사용합니다. 예를 들어, 버튼, 아이콘, 심벌 또는 기타 재사용 가능한 구성 요소가 있습니다. 디자인 전체에서 일관성을 유지하는 역할을 합니다.
* **메인 페이지:** 두 번째 페이지는 프로젝트의 완전한 사용자 인터페이스를 보여주는 메인 페이지입니다. 전체 앱 프로토타입을 사용하려면 ***재생*** 버튼을 누르세요.
* **기능 페이지:** 다른 페이지는 일반적으로 진행중인 기능에 전용되어 있습니다. 이 페이지들은 애플리케이션이나 웹사이트의 특정 기능 또는 모듈의 디자인을 포함하고 있습니다. 일반적으로 아직 진행 중인 상태입니다.
## 유용한 팁
읽기 전용 접근 권한으로는 디자인을 편집할 수 없지만, 디자인을 코드로 변환하는 데 유용한 모든 기능에 접근할 수 있습니다.
### 개발 모드 사용
Figma의 Dev Mode는 쉬운 디자인 탐색, 효과적인 자산 관리, 효율적인 통신 도구, 도구 상자 통합, 빠른 코드 스니펫, 주요 레이어 정보를 제공하여 디자이너와 개발자 간의 격차를 줄입니다. Dev Mode에 대해 더 알고 싶으시면 [여기](https://www.figma.com/dev-mode/)를 방문하세요.
툴바의 오른쪽에서 "개발자" 모드로 전환하여 디자인 사양을 보고 CSS를 복사하고 자산에 접근하세요.
### 프로토타입 사용
캔버스의 아무 요소나 클릭하고 인터페이스의 오른쪽 상단 모서리에 있는 "재생" 버튼을 눌러 프로토타입 보기에 접근하세요. 프로토타입 모드는 디자인을 최종 제품처럼 상호작용할 수 있게 합니다. 이를 통해 화면 간의 흐름과 버튼, 링크, 메뉴 등과 같은 인터페이스 요소가 상호작용할 때 어떻게 작동하는지 보여줍니다.
1. **전환 및 애니메이션 이해하기:** 프로토타입 모드에서는 디자이너가 화면 또는 UI 요소 간에 추가한 전환이나 애니메이션을 볼 수 있어 개발자에게 의도된 동작 및 스타일에 대한 명확한 시각적 지침을 제공합니다.
2. **실행 명확화:** 프로토타입은 모호성을 줄이는 데 도움을 줄 수 있습니다. 개발자는 특정 요소의 기능이나 모양을 보다 잘 이해하기 위해 이를 상호작용할 수 있습니다.
Figma 플랫폼 학습에 대한 포괄적인 세부 정보와 지침을 얻으시려면 [공식 Figma 문서](https://help.figma.com/hc/en-us)를 방문하세요.
### 거리 측정
요소를 선택하고 `Option` 키(Mac) 또는 `Alt` 키(Windows)를 누른 상태에서 다른 요소 위에 마우스를 올리면 요소 간 거리를 확인할 수 있습니다.
### VSCode용 Figma 확장 프로그램 (추천)
[VS Code용 Figma](https://marketplace.visualstudio.com/items?itemName=figma.figma-vscode-extension) 확장 프로그램은 텍스트 편집기에서 벗어나지 않고 디자인 파일 탐색 및 점검, 디자이너와의 협업, 변경 사항 추적, 구현 속도 향상을 가능하게 합니다.
추천하는 확장 프로그램의 일부입니다.
## 협업
1. **댓글 사용하기:** 툴바 왼쪽의 말풍선 아이콘을 클릭하여 댓글 기능을 사용할 수 있습니다.
2. **커서 채팅:** Figma의 멋진 기능 중 하나는 커서 채팅입니다. Figma를 다른 누군가와 동시에 사용하는 것을 보면 `;` (Mac)이나 `/` (Windows) 키를 눌러 메시지를 보낼 수 있습니다.