사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
앱 & 라이브러리▾
도메인▾
스칼라▾
ui 폴더 개요
ui/에는 화면 조각을 그리되 특정 모델에 묶이지 않는 컴포넌트를 둡니다. 페이지와 모듈 컴포넌트는 마크업을 반복하지 않고 이름으로 가져다 씁니다.앱 UI
관리자 헤더, 랜딩 히어로, 대시보드 위젯, 그 앱에만 있는 인터랙션처럼 한 앱이 가진 컴포넌트입니다. 폴더는 얕게 유지합니다.
@apps/myapp/ui라이브러리 UI
권한 게이트, 반응형 래퍼, 에디터 조각, 공통 폼 필드처럼 여러 앱이 함께 쓰는 컴포넌트입니다.
@libs/shared/ui이 페이지에서 쓰는 말
용어설명
barrel
폴더 안 파일을 다시 export하는
index.ts입니다. 쓰는 쪽은 @apps/myapp/ui 경로 하나로 import합니다."use client"
파일을 클라이언트 컴포넌트로 만드는 첫 줄입니다. 이 줄이 없으면 컴포넌트는 서버에서 그려집니다.
묶음 컴포넌트
여러 컴포넌트를 객체 하나로 묶어 export한 것입니다.
Only.Web, Chart.Bar처럼 씁니다.보조 파일
컴포넌트 하나를 돕는 camelCase 헬퍼나 타입 파일입니다.
swipeCard.util.ts가 그 예입니다.ui/에 둘까요?
두 가지를 물어보세요. JSX를 그리거나 모양을 정의하는가, 모델 하나를 받는가. 앞은 예, 뒤는 아니오일 때만 ui/에 둡니다.
코드
ui/
lib/<model>/
webkit/
JSX를 그리거나 모양을 정의하고 모델에 묶이지 않음 — ui/
랜딩 히어로 · 관리자 헤더
✓
한 앱에만 속하므로
apps/<app>/ui에 둡니다.Only.Admin · Only.Web
✓
여러 앱이 함께 쓰는 권한 게이트와 반응형 래퍼이므로
libs/<lib>/ui에 둡니다.Chart · MapView · Editor
✓
페이지와 모듈 파일이 직접 import할 수 없는 외부 패키지를 감쌉니다.
cardRecipe · panelRecipe
✓
여러 화면이 함께 쓰는 모양입니다. 컴포넌트도 hook도 아니지만
ui/Recipe/에 둡니다.다른 곳에 두는 것
주문 하나를 그리는 카드
✓
모델에 묶여 있으므로
lib/order/의 Order.Unit.tsx입니다.useGeoLocation
✓
자기 마크업이 없는 hook이나 브라우저 헬퍼입니다.
✓여기에 둡니다여기가 아닙니다
권장 구조
규칙은 단순합니다. 파일 하나에 export 하나, 파일 이름이 곧 export 이름입니다. 보통의
ui/ 폴더는 이렇게 생겼습니다:apps/myapp/ui
항목설명
AutoClose.tsx
파일 하나에 컴포넌트 하나이고, 파일 이름이 곧 export 이름입니다. PascalCase로 씁니다.
swipeCard.util.ts
역할 접미사를 붙인 camelCase 보조 파일입니다. 자기 컴포넌트만 상대 경로로 import합니다.
Only/index.tsx
묶음 컴포넌트입니다. 이
index.tsx는 직접 작성하며 "use client"를 달지 않습니다.Chart/index_.tsx
무거운 컴포넌트를
lazy()로 불러오는 "use client" 쪽 파일입니다. 옆에 서버에서 안전한 index.tsx를 둡니다.Recipe/
panelRecipe 같은 Tailwind variant 레시피입니다. 파일 하나에 하나씩 두고 Recipe/index.ts에서 다시 export합니다.tokens.css
lib 전용입니다. 테마를 따라가면 안 되는
:root 색을 두고, bg-[var(--kakao)]처럼 씁니다.index.ts
barrel입니다. 자동으로 만들어지므로 손으로 고치지 않습니다.
기본은 서버 컴포넌트
마크업만 그리는 컴포넌트에는
"use client"가 없습니다. 그래서 HTML로만 도착하고 번들에는 아무것도 더하지 않습니다:apps/myapp/ui/HomeHeader.tsx
- Props 인터페이스는 바로 위에.
interface HomeHeaderProps는 빈 줄 없이 컴포넌트 바로 위에 두고,className을 맨 앞에 씁니다. - 호출한 쪽의 class는 마지막에.
cn(base, className)으로 합치면 페이지가 prop을 더 만들지 않고도 모양을 조정할 수 있습니다. - 문자열 대신 슬롯.
title과right는ReactNode이므로, 페이지가 번역된 문구나 다른 컴포넌트를 넘길 수 있습니다.
브라우저가 필요할 때
AutoClose는 useEffect와 window를 쓰므로 첫 줄에 "use client"가 필요합니다:apps/myapp/ui/AutoClose.tsx
- 기본값은 구조 분해에서.
timeout = 0으로 쓰고,defaultProps나React.FC는 쓰지 않습니다. - effect는 스스로 정리합니다.
clearTimeout을 반환하면 컴포넌트가 먼저 사라질 때 닫기가 취소됩니다.



ui/에는 async 컴포넌트를 두지 않습니다. React에는 async 클라이언트 컴포넌트가 없어서, 클라이언트 부모가 그리는 순간 깨집니다. page에서 await하고 결과를 prop으로 넘기세요. async 형태는 lint가 막습니다.
barrel에서 import
ui/의 컴포넌트는 파일 경로가 아니라 폴더의 barrel을 거쳐 import합니다. 페이지에서는 AutoClose를 이렇게 씁니다:apps/myapp/page/signin/done.tsx
ui/의 파일 종류마다 import하는 쪽에서는 이렇게 보입니다:
| ui/의 파일 | 쓰는 법 |
|---|---|
| ui/AutoClose.tsx | import { AutoClose } from "@apps/myapp/ui" |
| ui/Only/index.tsx | import { Only } from "@libs/shared/ui" 한 뒤 <Only.Web>으로 씁니다. |
| ui/Recipe/panel.ts | Recipe/index.ts를 거쳐 import { panelRecipe } from "@apps/myapp/ui"로 씁니다. |
| ui/swipeCard.util.ts | barrel에 없습니다. SwipeCard.tsx가 ./swipeCard.util로 import합니다. |
- 맨 위의 PascalCase 이름만.
ui/바로 아래에 있는 PascalCase 파일과 폴더만 export되고, camelCase 보조 파일은 밖으로 나가지 않습니다. ui/index.ts는 고치지 않습니다. 컴포넌트 파일을 추가하거나 이름을 바꾸세요.akan start중에는 저장할 때 barrel이 갱신되고, 그 밖에는akan sync를 실행합니다.- 깊은 경로는 쓰지 않습니다.
@apps/myapp/ui/AutoClose는 barrel을 건너뛰는 경로라서, 페이지와 모듈 파일에서는 lint 오류입니다.
묶음 컴포넌트
Only.Admin, Only.Web처럼 이름 하나로 묶어야 더 잘 읽히는 컴포넌트가 있습니다. 이런 컴포넌트는 폴더를 만들고, 그 폴더의 index.tsx에서 이름 하나로 export합니다.멤버는 각각 평범한 컴포넌트 파일입니다.
Web은 store를 읽으므로 클라이언트 파일입니다:libs/shared/ui/Only/Web.tsx
폴더의
index.tsx는 멤버를 객체 하나로 모으고, "use client"를 달지 않습니다:libs/shared/ui/Only/index.tsx
페이지는 이름 하나를 import하고 멤버를 골라 씁니다:
apps/myapp/page/_index.tsx
{ agent: false }는 너비를 감춥니다. 컴포넌트는innerWidth를 그대로 구독하지만, 인페이지 에이전트는 이 값을 읽지 못합니다.- 이
index.tsx는 직접 쓰는 파일입니다. 자동으로 만들어지는ui/index.ts와 달리, 폴더의 묶음 파일은 직접 작성하고 고치는 평범한 소스입니다.



묶음 객체를
"use client" 파일에서 만들지 마세요. 서버 컴포넌트는 클라이언트 파일의 export를 각각 속을 볼 수 없는 스텁(stub) 하나로 받으므로, Only 객체 전체가 스텁 하나가 되어 Only.Web은 undefined가 됩니다. "use client"는 멤버 파일에만 달고, 객체는 이 줄이 없는 index.tsx에서 만드세요.무거운 컴포넌트: index_.tsx 쌍
차트, 지도, 에디터는 import하는 순간
window를 건드리기 쉬운 큰 패키지를 끌고 옵니다. 이럴 때는 폴더의 진입 파일을 둘로 나눕니다. 먼저 index_.tsx가 멤버를 lazy()로 불러옵니다:libs/util/ui/Chart/index_.tsx
그다음
"use client"가 없는 index.tsx가 앱의 다른 파일이 import할 묶음을 만듭니다:libs/util/ui/Chart/index.tsx
- 브라우저 전용 패키지에는
ssr: false. 서버는loading대체 화면을 그리고, 실제 차트는 브라우저에서 마운트됩니다. lazy()대상은export default를 씁니다../Bar는 컴포넌트를 모듈의 default로 export합니다.- 두 파일을 합치지 않습니다. 합치면 묶음이 클라이언트 파일에 들어가서, 위 경고와 같은 이유로 깨집니다.
실전 규칙
- 파일 하나, export 하나, 같은 이름.
AutoClose.tsx는AutoClose를 export합니다. - 앱 ui/는 얕게.
Only.Web,Only.Admin처럼 자연스럽게 묶이는 API일 때만 폴더를 만듭니다. - barrel에서 import. 파일까지 들어가는 경로 대신
@apps/myapp/ui나@libs/shared/ui를 씁니다. - 서버가 먼저.
"use client"는 hook, 이벤트 핸들러, store, 브라우저 전역 객체, 클라이언트 전용 패키지를 쓸 때만 답니다.
자주 하는 실수
| 실수 | 고치는 법 |
|---|---|
| 마크업만 그리는 컴포넌트에 "use client"를 단다 | hook, 이벤트 핸들러, store, 브라우저 전역 객체, 클라이언트 전용 패키지를 쓰지 않는다면 지웁니다. |
Order 하나를 받는 카드를 ui/에 둔다 | lib/order/Order.Unit.tsx로 옮깁니다. |
| 컴포넌트를 파일 경로로 import한다 | @apps/myapp/ui/AutoClose 대신 @apps/myapp/ui를 씁니다. 깊은 경로는 lint 오류입니다. |
ui/index.ts에 직접 줄을 추가한다 | 그대로 둡니다. 대신 컴포넌트 파일을 추가하거나, 이름을 바꾸거나, 지웁니다. |
"use client" 파일에서 Only = { … }를 만든다 | "use client"가 없는 index.tsx에서 객체를 만듭니다. |
ui/에 async 컴포넌트를 쓴다 | page에서 await하고, 결과를 prop으로 넘깁니다. |
page나 *.Unit.tsx에서 외부 패키지를 import한다 | lib의 ui/ 컴포넌트로 감싸고, 그 컴포넌트를 import합니다. |
| 같은 카드 클래스를 여러 파일에 복사한다 | ui/Recipe/에 레시피 하나를 만들고 모든 곳에서 호출합니다. |
함께 볼 문서