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이나 브라우저 헬퍼입니다.
✓여기에 둡니다여기가 아닙니다

barrel에서 import

ui/의 컴포넌트는 파일 경로가 아니라 폴더의 barrel을 거쳐 import합니다. 페이지에서는 AutoClose를 이렇게 씁니다:
apps/myapp/page/signin/done.tsx
ui/의 파일 종류마다 import하는 쪽에서는 이렇게 보입니다:
ui/의 파일쓰는 법
ui/AutoClose.tsximport { AutoClose } from "@apps/myapp/ui"
ui/Only/index.tsximport { Only } from "@libs/shared/ui" 한 뒤 <Only.Web>으로 씁니다.
ui/Recipe/panel.tsRecipe/index.ts를 거쳐 import { panelRecipe } from "@apps/myapp/ui"로 씁니다.
ui/swipeCard.util.tsbarrel에 없습니다. 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와 달리, 폴더의 묶음 파일은 직접 작성하고 고치는 평범한 소스입니다.
무거운 컴포넌트: 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/에 레시피 하나를 만들고 모든 곳에서 호출합니다.
함께 볼 문서

MIT 라이선스 하에 배포되었습니다.

내 AI에 이 문서 연결하기

MCPhttps://akanjs.com/mcp
Copyright © 2026 Akan.js 모든 권리 보유.시스템 관리자bassman