사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
앱 & 라이브러리▾
도메인▾
스칼라▾
webkit 개요
webkit/에는 여러 화면이 함께 쓰지만 컴포넌트는 아닌 브라우저 코드를 둡니다. 서버 전용 코드를 두는 srvkit/의 브라우저 쪽 짝입니다.덕분에 페이지와 컴포넌트는 로직을 직접 들고 있지 않고, webkit barrel에서 이름 하나만 가져다 씁니다.
이 페이지에서 쓰는 말
용어설명
barrel
폴더 안의 모든 파일을 다시 export하는
index.ts입니다. 쓰는 쪽은 폴더 경로 하나로 import합니다."use client"
파일을 클라이언트 코드로 표시하는 첫 줄입니다. 서버 컴포넌트는 이 파일의 함수를 호출하거나 값을 읽을 수 없습니다.
서버 컴포넌트
page,
Unit, View입니다. 서버에서 그려져 브라우저에는 HTML로만 도착합니다.어느 폴더에 둘까
폴더는 코드의 용도가 아니라 코드가 건드리는 것으로 고릅니다.
common/, webkit/, srvkit/ 문서가 모두 이 표로 시작합니다.폴더설명
common/
서버와 클라이언트 양쪽에서 도는 순수 코드이며 외부 의존성이 없습니다. 같은 폴더의
common/*과 akanjs/base만 import하고, Err는 쓸 수 없습니다.webkit/
window, navigator, 네이티브 브리지(akanjs/client/native)를 건드리거나 React hook인 코드입니다.srvkit/
node:*, Bun, process.env, 비밀값, 서버 SDK 중 하나라도 건드리는 코드입니다.ui/
JSX를 그리거나 레시피로 모양을 정의하되 모델 하나에 묶이지 않는 코드입니다. 모델에 묶인 컴포넌트는 그 모듈에 둡니다.
plugin/
빌드나 CLI 실행 때 도는
AkanPlugin입니다. akan.config.ts에 등록합니다.webkit에 두는 것
여기에는 다섯 종류의 코드를 둡니다. 파일 첫 줄에
"use client"가 있는지에 따라 쓸 수 있는 쪽이 정해집니다:종류
서버
page · Unit · View
클라이언트
"use client"
"use client" 없음 — 서버와 클라이언트 모두
표시용 맵
✓
✓
상태별 배지 variant 이름이나 아이콘처럼, enum 값을 고르는 공용 표입니다. 클래스 문자열은 담지 않습니다.
계정·라우팅 헬퍼
✓
✓
로그인한 계정을 읽고 비로그인 사용자를 돌려보냅니다.
_layout.tsx의 getSelf가 그 예입니다."use client" 있음 — 클라이언트 컴포넌트에서만
브라우저 헬퍼
✓
텍스트 복사, 파일 다운로드, 쿠키 읽기, 공유 링크 열기 같은 작은 브라우저 동작입니다.
웹 hook
✓
뷰포트, 권한, 알림, 메시징 같은 브라우저 API를 감싼 재사용 hook입니다.
외부 라이브러리 래퍼
✓
브라우저용 외부 패키지를 직접 만든 함수 뒤에 숨겨, 페이지가 그 패키지를 import하지 않게 합니다.
✓사용 가능사용 불가


서버 컴포넌트가 읽는 파일에는
"use client"를 붙이지 않습니다. 서버에서는 "use client" 파일의 export가 자리표시자로 바뀌어, 호출하면 에러가 나고 맵의 키는 undefined로 읽힙니다. 그래서 표시용 맵과 계정 헬퍼에는 "use client"가 없습니다.표시용 맵
여러 모듈이 함께 쓰는 상태별 배지 표입니다:
apps/koyo/webkit/icecreamOrderStatusVariant.ts
- 두 번째 모듈이 쓸 때 옮깁니다. 한 모듈만 쓰는 표는 그 파일 최상단의
as const맵으로 둡니다. 다른 모듈도 필요해지면webkit/으로 옮깁니다. - 클래스 문자열은 레시피에 둡니다. 값이 클래스인 표는 variant 축이므로 여기가 아니라
ui/Recipe/의 레시피로 옮깁니다. - 글자는 dictionary에서 가져옵니다. 맵에는 모양만 두고, 글자는
l("icecreamOrderStatus.active")로 그립니다. 사용자가 읽는 문구는 하드코딩하지 않습니다. satisfies가 모든 상태를 확인합니다. enum에 상태를 추가하고 여기에 variant를 빠뜨리면 타입 에러가 납니다.
계정·라우팅 헬퍼
로그인한 계정을 읽고, 로그인하지 않은 사용자는 로그인 페이지로 보내는 헬퍼입니다:
apps/koyo/webkit/getSignedInUser.ts
"use client"가 없습니다._layout.tsx가 서버에서 호출하므로, 로그인하지 않은 사용자는 HTML이 나가기 전에 로그인 페이지로 이동합니다.router.replace가 아니라router.redirect를 씁니다.redirect는 서버와 브라우저 양쪽에서 동작하지만,replace는 브라우저 탭만 옮깁니다. 서버에서 부르면 로그아웃 상태 그대로 페이지가 그려집니다.libs/shared에 이미 있습니다. 앱이 그 lib을 쓴다면@libs/shared/webkit의getSelf({ unauthorize: "/signin" })와getMe를 씁니다.
브라우저 헬퍼
페이지마다 반복될 브라우저 동작 하나를 함수로 둡니다:
apps/koyo/webkit/copyText.ts
navigator를 건드리므로"use client"가 붙습니다.onClick핸들러처럼 클라이언트 컴포넌트에서만 부를 수 있습니다.- 먼저
@libs/shared/webkit을 확인하세요.downloadFile, JSON 내보내기용downloadData, 파일을 올리고 준비될 때까지 기다리는addFileUntilActive가 이미 있습니다.
웹 hook
브라우저 이벤트를 구독하고, 끝나면 스스로 정리하는 hook입니다:
apps/koyo/webkit/useViewportWidth.tsx
- 튜플이 아니라 이름 있는 객체를 반환합니다. 호출하는 쪽은
const { width } = useViewportWidth()로 씁니다. - 이미 있는 hook부터 확인하세요.
akanjs/webkit에는useDebounce,useThrottle,useInterval,useEscapeKey가 있고,useCamera,useGeoLocation으로 네이티브 런타임과 브라우저 API를 감싸 둡니다.@libs/util/webkit에는usePushNotification,useSpeech가 더 있습니다. - 진짜 상태가 필요할 때만 씁니다. 너비로 마크업을 보이고 숨기기만 한다면
md:hidden같은 반응형 class로 충분하고, 두 경우 모두 서버에서 그려집니다.
외부 라이브러리 래퍼
libs/shared의 downloadFile은 file-saver를 감쌉니다:libs/shared/webkit/downloadFile.ts
- 페이지와 모듈 파일은 외부 패키지를 import하지 않습니다.
page/**,*.Zone.tsx,*.store.ts에서 외부 패키지를 import하면 lint 에러가 나므로, 대신downloadFile을 import합니다. - 외부 컴포넌트를 감쌀 때는
ui/에 둡니다.webkit/은 함수, hook, 맵처럼 camelCase 이름만 export합니다.qrcode.react를 감싼libs/util/ui/QRCode.tsx처럼, 컴포넌트 래퍼는ui/의 PascalCase 파일로 둡니다.
barrel과 파일 이름
webkit/은 ui/처럼 barrel 폴더라서, 모든 파일이 진입점 하나에서 다시 export됩니다. 파일 이름은 그 파일이 export하는 이름 하나를 따릅니다:파일 이름설명
use<Thing>.tsx
React hook입니다. JSX가 없어도 확장자는
.tsx입니다.<camelName>.ts
나머지 헬퍼, 래퍼, 맵입니다. 파일 하나에 export 하나를 두고, 이름은 파일명과 같게 합니다.
컴포넌트는 헬퍼를 barrel 경로로 가져옵니다:
apps/koyo/ui/DownloadButton.tsx
- barrel 경로만 씁니다.
@libs/shared/webkit이나@apps/koyo/webkit으로 씁니다.@libs/shared/webkit/downloadFile처럼 더 깊은 경로는 페이지와 모듈 파일에서 lint 에러가 납니다. - 클릭은 버튼이 맡습니다.
onClick때문에"use client"가 필요하고,Button은 반환된 promise가 끝날 때까지 스피너를 보여 줍니다. - 페이지는
file-saver를 모릅니다. 페이지는<DownloadButton />만 그리고, 외부 패키지는webkit/뒤에 남습니다.
실전 규칙
webkit 파일은 거의 다 이 여섯 가지 규칙으로 정리됩니다:
- 로직은
webkit/, 컴포넌트는ui/. 웹 렌더링에 필요하지만 그 자체가 재사용 UI 컴포넌트는 아닌 로직을 여기에 둡니다. - 브라우저 코드는
webkit/, 서버 전용 코드는srvkit/.node:*,Bun,process.env, 비밀값을 건드리면srvkit/에 둡니다. - barrel에서 import합니다. 안쪽 경로가 아니라
@libs/shared/webkit에서 가져옵니다. - 파일 이름과 export 이름을 맞춥니다.
downloadFile.ts는downloadFile을 export합니다. "use client"는 브라우저가 필요한 곳에만 붙입니다. hook, 브라우저 API, 브라우저용 패키지에는 필요하고, 서버 컴포넌트가 읽는 파일에는 붙이면 안 됩니다.//!주석을 쓰지 않습니다. Bun이 압축 뒤에도 남겨 두기 때문에 모든 방문자에게 그대로 전송됩니다. 대신// FIXME:를 씁니다.
webkit/을 import할 수 있는 곳
서버 파일과 공유 파일은
import type으로 webkit의 타입만 가져올 수 있고, 값은 import할 수 없습니다:import하는 파일
값
import { x }
타입
import type { X }
화면 쪽 파일
page/ · ui/
✓
✓
import할 수 있습니다. 단, 서버 컴포넌트는 "use client"가 없는 export만 호출합니다.
<Model>.*.tsx · *.store.ts
✓
✓
모듈 컴포넌트와 store는
@libs/util/webkit 같은 barrel에서 import합니다.서버 파일과 공유 파일
*.service.ts · *.document.ts
✓
서버 코드는 DOM이 없는 Bun에서 돌기 때문에 webkit의 타입만 가져올 수 있습니다.
*.signal.ts · *.dictionary.ts
✓
계약 파일도 서버에서 로드되므로 같은 규칙을 따릅니다.
srvkit/
✓
서버 전용 헬퍼와 어댑터는 브라우저 코드를 불러오지 않습니다.
common/ · *.constant.ts
✓
공유 파일은 양쪽에서 돌기 때문에
webkit/에도 srvkit/에도 닿지 않습니다.✓허용lint 에러



webkit/도 서버 코드를 import할 수 없습니다. srvkit/, *.service.ts, *.document.ts, *.signal.ts, *.dictionary.ts, 서버 진입점, db / srv / sig / dict / option / useServer barrel을 값으로 import하면 lint 에러가 납니다. 모델은 클라이언트 진입점의 cnst에서 읽고, 타입만 필요하면 import type을 씁니다.함께 볼 문서