Skip to content

AppHeader

진행 중 (AppHeader component — work in progress)

  • 모바일 앱 셸 상단 바. 상단 safe-area(노치)를 흡수하고, 좌측 텍스트 · 액션 버튼 · 알림벨 · 아바타를 갖는 앱 크롬.
  • 기본형은 배경·하단선이 없습니다 — 셸 배경과 이어지고 하단 패딩도 0이라 높이 48px(예전 57px에서 축소). 배경(solid)과 하단선(divider)은 각각 독립으로 켤 수 있어요(배경과 구분선).
  • 1depth / 2depth 두 형태가 있습니다. onBack을 넘기면 2depth로, 좌측에 백버튼이 붙고 제목이 강조돼요(깊이).
  • ⚠️ 탭도 이 컴포넌트가 그립니다. 모바일에서 상단 내비게이션은 헤더 줄 + 탭 줄이 하나예요 — 웹처럼 본문에 nav.ds-tab-nav를 따로 두지 않습니다(탭 줄).
  • 슬롯은 전부 옵션으로 켜고 끕니다onBack / showBell / showAvatar / getActions. 우측 버튼 추가는 배열에 항목을 더하는 것으로 끝나요.
  • 모든 색·크기·타이포는 .ds-app-header* 토큰에서 옵니다 (인라인 스타일·하드코딩 hex·!important 없음).
  • 현재 소비 앱은 employee-app 하나입니다.
규칙 (강제) · 코딩 에이전트용 — 사람은 접힌 채로 두어도 됩니다

AppHeader 강제 규칙. 상세는 아래 본문.

⚠️ 트랩 1 — PageHeader와 다른 컴포넌트입니다. 섞어 쓰지 마세요.ds-page-header데스크톱 크롬이에요 — 3영역(타이틀바/툴바), 팀 셀렉터, 프로필 드롭다운, 44px 헤더 높이 정렬이 전제입니다. 모바일에 그대로 쓰면 툴바가 넘칩니다. 반대로 .ds-app-header를 데스크톱에 쓰면 safe-area 패딩이 의미 없이 붙어요. 화면 폭이 아니라 "무슨 셸인가"로 고르세요.

마크업

  • 호스트가 x-data="appHeader({...})"로 감싸고 packages/components/html/app-header.html을 include 합니다. 손으로 짓지 마세요.
  • 래퍼에 class="contents"가 필요합니다. 앱 셸 컨테이너가 display:flex; flex-direction:column 이면 x-data 래퍼 <div>가 flex 자식으로 끼어들어 헤더 높이 계산이 어긋납니다. display:contents로 자식 <header>를 직접 flex 자식으로 올리세요.
  • 알림벨은 중첩 include 로 들어갑니다. htmlInclude()는 재귀 처리하지만 경로 기준(root)은 앱 디렉터리예요 — 그래서 partial 안에서도 "../../packages/components/html/notification-bell.html" 처럼 앱 기준 상대경로를 씁니다. 컴포넌트 기준 상대경로(./notification-bell.html)로 쓰면 빌드가 파일을 못 찾습니다.

⚠️ 트랩 2 — 아바타는 스토어 경로를 쓰세요. getFileUrl()을 직접 부르지 마세요getAvatarImage를 생략하면 컴포넌트가 스토어의 currentUserProfilePictureUrl(presigned·cache-busted)을 씁니다. PageHeader와 같은 경로이고, 사진을 교체했을 때 refreshProfilePictureUrl() 갱신이 자동으로 반영돼요. getFileUrl()을 직접 호출해 x-effect로 넣으면 그 갱신·캐시 무효화를 우회합니다(이 앱이 실제로 그랬고, 컴포넌트화하면서 걷어냈습니다).

기본형은 배경·하단선 없음, 하단 패딩 0padding: max(env(safe-area-inset-top, 0px), 12px) 16px 0. 상단만 safe-area와 12px 중 큰 값을 갖습니다.

  • 초기 구현은 12px 16px 8px + 흰 배경 + 하단선 1px = 57px였고, 기본형은 지금 48px입니다.
  • 아래 콘텐츠와의 간격은 콘텐츠가 갖습니다. 헤더에 하단 패딩을 되돌리면 화면마다 간격이 이중으로 붙어요.
  • 배경·하단선이 필요하면 CSS를 고치지 말고 solid·divider 옵션을 쓰세요 (배경과 구분선). 앱 CSS에서 .ds-app-headerbackground를 덧칠하면 두 변형이 갈라집니다.

탭 줄 — 웹 탭과 다른 두 가지

  • 배경 틴트 없음. .ds-app-header__tabs 안에서는 hover·pressed 배경을 깔지 않습니다(웹 TabNav는 --ds-hover-tint-16/30 사용). 터치 기기에서 :hover가 탭한 뒤에도 남아 선택하지 않은 탭에 회색 박스가 붙은 것처럼 보이기 때문입니다. 선택 피드백은 글자색 + 밑줄이 담당해요. @media (hover: hover)로 가르지 마세요 — 데스크톱 브라우저 검수 화면과 실기기가 갈립니다 (배경 틴트 없음).
  • 활성 밑줄 오프셋이 1px 다릅니다. 웹 값(ml -15px)은 nav 하단 1px 선에 겹치라고 내려간 값이라, 선이 없고 overflow-y: clip이 걸린 헤더에서는 밑줄 아래가 잘립니다. DS가 .ds-app-header__tabs 안에서 -14px로 보정해 두었으니 앱에서 다시 만지지 마세요.

알림벨 글리프 크기

  • .ds-app-header__bell > .relative > button > svg 가 24px로 키웁니다(모바일 터치 타깃). 공유 벨은 다른 곳에서 20px 그대로예요.
  • 이전에는 [x-data="notificationBell"] > .relative > button > svg앱 CSS에서 잡았는데, 공유 템플릿의 4단 중첩 구조에 의존해 구조가 바뀌면 조용히 깨졌습니다. 클래스 훅으로 대체했으니 속성 셀렉터로 되돌리지 마세요.

시각 미리보기

라이브 미리보기 — 프로덕션과 동일한 .ds-app-header* 클래스, 실제 앱 셸과 같은 430px 폭입니다. 2depth 체크박스로 백버튼과 제목 강조를, 나머지 체크박스로 슬롯(액션 · 벨 · 아바타)을 켜고 꺼 보세요. 우측은 액션 → 벨 → 아바타 순서로 고정입니다.

배경이 없어 헤더가 셸 배경(회색) 위에 그대로 얹히는 것도 여기서 확인됩니다 — 아래 흰 본문 영역과의 경계에 선이 없어요.

2026년 7월 20일 월
본문 영역

체크박스로 슬롯을 켜고 꺼 보세요 — 우측은 액션 → 벨 → 아바타 순서가 고정입니다

데모의 한계

알림벨은 실제로는 Alpine 공유 컴포넌트(notificationBell)를 중첩 include 하는 자리라, 여기서는 같은 DOM 구조만 정적으로 재현했습니다 — 드롭다운은 열리지 않습니다. 상단 safe-area 패딩도 데스크톱 브라우저에서는 max(0px, 12px) = 12px로만 보입니다(노치 기기에서 더 커집니다).

단일 출처

파트파일
Alpine 컴포넌트packages/components/src/components/AppHeader.ts
HTML partialpackages/components/html/app-header.html
스타일packages/static/styles/design-system.css (.ds-app-header*)
Alpine 등록appHeader (plugin.ts)

토큰 소유권: color·typography·geometry(아바타 32px·gap 8px·safe-area 패딩)는 모두 DS 소유(.ds-app-header*). arbitrary [...]·하드코딩 hex·새 !important 없음.

PageHeader와의 차이

두 컴포넌트는 이름이 비슷하지만 다른 셸을 위한 것입니다.

.ds-app-header (모바일).ds-page-header (데스크톱)
Capacitor 모바일 앱브라우저 데스크톱
safe-area상단 env(safe-area-inset-top) 흡수없음
구성leading 텍스트 · 액션 · 벨 · 아바타타이틀바 + 툴바(팀 셀렉터·프로필 드롭다운·divider)
높이내용에 따름 (패딩 기반)44px 정렬 전제
하단 짝BottomNavLnbSidebar

모바일 앱에 PageHeader를 넣으면 툴바가 넘치고, 데스크톱에 AppHeader를 넣으면 safe-area 패딩이 무의미하게 붙습니다.

구조

┌─ header.ds-app-header ───────────────────────────────────────┐
│  padding: max(env(safe-area-inset-top), 12px) 16px 0         │
│  배경 없음 · 하단선 없음 · 총 높이 48px                        │
│ ┌─ .ds-app-header__row (flex, space-between, gap 8) ───────┐ │
│ │ ┌ .__lead ─────────────┐      .ds-app-header__actions    │ │
│ │ │ [.__back 32px] (2depth)│    ├ [액션 버튼 …]             │ │
│ │ │ .__leading             │    ├ [.__bell  알림벨 24px]    │ │
│ │ └────────────────────────┘    └ [.__avatar 32px 원형]     │ │
│ └──────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘

좌측 __lead백버튼 + 제목 묶음, 우측 __actions액션 → 벨 → 아바타 순으로 고정입니다.

깊이 (1depth / 2depth)

onBack을 넘기는지로 갈립니다. 별도 depth 옵션은 없어요 — 뒤로 갈 곳이 있으면 2depth라는 뜻입니다.

1depth2depth
조건onBack 없음onBack 지정 (+ showBack 미지정/true)
좌측__leading 텍스트만__back 버튼 + __leading
__leading 성격보조 텍스트 (날짜 등)화면 제목
스타일b4(14px) · neutral-400b3(16px) · semibold · neutral-900
루트 클래스.ds-app-header--depth2

3depth 이상도 2depth와 같은 형태입니다. 깊이를 세지 않고 "뒤로 갈 곳이 있는가"만 봅니다 — 백버튼 + 제목이면 충분하고, 계층이 깊어져도 헤더 모양은 달라지지 않아요.

아래는 실제 렌더입니다(프로덕션과 동일한 클래스).

1depth — 보조 텍스트(날짜) + 벨 + 아바타

2026년 7월 20일 월
본문 영역

체크박스로 슬롯을 켜고 꺼 보세요 — 우측은 액션 → 벨 → 아바타 순서가 고정입니다

2depth 이상 — 백버튼 + 화면 제목

연차 상세
본문 영역

체크박스로 슬롯을 켜고 꺼 보세요 — 우측은 액션 → 벨 → 아바타 순서가 고정입니다

2depth + 탭 줄 — 하위 화면에 탭이 있는 경우

결재
본문 영역

체크박스로 슬롯을 켜고 꺼 보세요 — 우측은 액션 → 벨 → 아바타 순서가 고정입니다

2depth에서 벨·아바타는 보통 끕니다(showBell: false, showAvatar: false) — 하위 화면에서 프로필로 빠지는 동선은 잘 안 쓰니까요. 위 예시도 꺼둔 상태입니다. 다만 강제는 아니라 필요하면 켤 수 있습니다.

헤더 하나가 목록과 상세를 겸할 때

앱 셸은 헤더 인스턴스가 하나인데 화면은 목록(1depth)과 상세(2depth)를 오갑니다. 이때는 onBack항상 넘기고 showBack으로 깊이를 가릅니다 — onBack 유무는 마운트 시점에 굳어 바꿀 수 없기 때문입니다.

html
<div class="contents" x-data="appHeader({
       getLeading: () => headerLeading,        <!-- 목록: 날짜 / 상세: 화면 제목 -->
       onBack: () => depthBack(),
       showBack: () => isDepth,                <!-- 여기서 깊이가 갈린다 -->
       showBell: () => !isDepth,
       showAvatar: () => !isDepth,
       getActions: () => headerActions,        <!-- 상세 전용 액션(검색·멤버 등) -->
     })">

제목 강조(--depth2)는 백버튼과 한 몸이라 showBack 결과를 따라갑니다. 목록에서 제목만 커지는 일은 생기지 않습니다.

상세 상태는 어디에 두나

상세를 여는 쪽(섹션/컴포넌트 스코프)과 헤더(앱 셸 스코프)는 서로를 못 봅니다. 상태는 앱의 라우터 스토어에, 뒤로가기 같은 동작은 window 이벤트로 넘기세요. 스토어에 뒤로가기 함수를 담으면 섹션 스코프가 스토어로 새어나가고, 섹션이 둘 이상이면 누구 것인지 추적이 안 됩니다.

탭이 필요한 화면getTabs를 넘기면 헤더가 탭 줄까지 그립니다 — 별도 컴포넌트를 놓지 않습니다(탭 줄).

배경과 구분선

배경(solid)과 하단선(divider)은 서로 독립적인 on/off 축입니다. 넷 중 아무 조합이나 가능하고, 기본형은 둘 다 없음이에요.

조합옵션높이 (탭 없음)높이 (탭 있음)쓰임
기본형48px48px + 탭 줄셸 배경과 이어져 화면의 일부처럼 읽힘
배경만solid56px48px + 탭 줄콘텐츠 위에 얹힌 면으로 읽힘
선만divider57px48px + 탭 줄 + 1배경 없이 경계만 필요할 때
배경+선둘 다57px48px + 탭 줄 + 1가장 또렷한 분리

하단 여백은 조건부입니다

배경이나 선이 있으면 헤더가 하나의 이 되므로, 콘텐츠와의 하단 여백 8px을 헤더가 직접 갖습니다. 기본형(투명)은 셸과 이어지므로 그 여백이 콘텐츠 몫이라 0이고요.

단 탭 줄이 있으면 붙지 않습니다 — 탭이 그 아래를 채우기 때문입니다. 8px을 더하면 탭과 콘텐츠 사이가 벌어져요. 조건은 이렇습니다:

(solid 또는 divider) AND 탭 줄 없음  →  padding-bottom: 8px

판정은 컴포넌트(appHeaderRootClass)가 하고 CSS는 결과 클래스(--pad-bottom)만 받습니다. :has()에 기대지 않으려는 의도예요.

참고: 초기 구현의 57px이 정확히 divider + 하단 8px 조합입니다. 처음부터 헤더는 "선 있는 변형"이었고, 그게 기본값처럼 굳어 있었던 셈이에요.

⚠️ dividerborder-bottom이라 높이에 1px이 더 붙습니다. 헤더 높이를 계산에 쓰는 곳이 있으면 감안하세요.

기본형 — 배경·선 없음 (48px)

2026년 7월 20일 월
본문 영역

체크박스로 슬롯을 켜고 꺼 보세요 — 우측은 액션 → 벨 → 아바타 순서가 고정입니다

배경만solid: true

2026년 7월 20일 월
본문 영역

체크박스로 슬롯을 켜고 꺼 보세요 — 우측은 액션 → 벨 → 아바타 순서가 고정입니다

선만divider: true

2026년 7월 20일 월
본문 영역

체크박스로 슬롯을 켜고 꺼 보세요 — 우측은 액션 → 벨 → 아바타 순서가 고정입니다

배경 + 선 — 둘 다 (57px)

2026년 7월 20일 월
본문 영역

체크박스로 슬롯을 켜고 꺼 보세요 — 우측은 액션 → 벨 → 아바타 순서가 고정입니다

배경 + 탭 줄 — 탭이 있으면 하단 8px이 붙지 않습니다

2026년 7월 20일 월
본문 영역

체크박스로 슬롯을 켜고 꺼 보세요 — 우측은 액션 → 벨 → 아바타 순서가 고정입니다

배경이 필요할 때 앱 CSS에서 .ds-app-headerbackground를 덧칠하지 마세요. 두 변형이 갈라져 나중에 어느 화면이 어떤 상태인지 추적이 안 됩니다 — 옵션으로만 켜세요.

화면마다 다르면 함수로 넘기세요

앱 셸(employee-app)처럼 헤더 인스턴스 하나가 여러 섹션을 도는 구조에서는 solid·dividerboolean이 아니라 () => boolean으로 넘깁니다. x-data 표현식은 마운트 시 한 번만 평가되므로, boolean을 넣으면 첫 화면의 값이 그대로 굳어 섹션을 바꿔도 배경이 따라오지 않습니다.

html
<!-- 메신저 섹션에서만 배경을 깐다(본문 맨 위가 흰 서브탭 바라 회색 띠가 남지 않도록) -->
<div class="contents" x-data="appHeader({
       solid: () => activeSection === 'messenger',
     })">

판단 기준은 본문 맨 위가 무슨 면인가입니다 — 흰 면(서브탭 바·카드)으로 시작하면 solid로 이어 붙이고, 회색 셸이 그대로 이어지면 기본형(투명)을 둡니다.

탭 줄

⚠️ 모바일에서 상단 내비게이션은 헤더와 탭이 한 컴포넌트입니다. 웹처럼 본문에 nav.ds-tab-nav를 따로 두지 마세요 — getTabs를 넘기면 헤더 줄 아래에 탭 줄이 붙습니다. 비우거나 생략하면 탭 줄 자체가 렌더되지 않습니다.

┌─ .ds-app-header ─────────────────┐
│  ‹  연차 상세          🔔  (A)   │  ← __row (헤더 줄)
│  연차   근무   인센티브   서류     │  ← __tabs (탭 줄, getTabs 있을 때만)
└──────────────────────────────────┘
js
appHeader({
  getLeading: () => '결재',
  getTabs: () => [
    { id: 'leave', label: '연차' },
    { id: 'work', label: '근무', badge: 2 },
  ],
  getActiveTab: () => activeTab,
  onTabSelect: (id) => switchTab(id),
})
AppHeaderTab타입설명
idstringonTabSelect 인자이자 활성 비교 대상
labelstring탭 라벨
badgenumber | string | null후행 카운트. 0·null·"" 면 숨김
disabledboolean?렌더하되 선택 차단

아이템 스타일은 웹 TabNav와 공유합니다.ds-tab-item + .ds-tab-item--ml(48px)을 그대로 씁니다. 형태를 웹과 맞추기 위해서예요.

  • ⚠️ 활성 클래스는 tab-active 입니다. is-active가 아니에요(BottomNav와 다릅니다 — 그쪽은 is-active).
  • 사이즈는 현재 ml 고정입니다. 모바일 전용 사이즈 베리에이션이 정해지면 여기에 축을 추가합니다.
  • 가로 스크롤은 .ds-app-header__tabs가 소유하며, 활성 밑줄이 박스를 넘어 세로 스크롤을 만드는 문제는 overflow-y: clip으로 DS 안에서 막았습니다. 앱 CSS에 클립 보정을 따로 넣지 마세요(웹은 반대로 .tabnav-unclip으로 클립을 푸는 opt-in 보정을 쓰는데, 그 규칙을 여기 복사하면 세로 스크롤이 되살아납니다).
  • 활성 밑줄 위치도 웹과 다릅니다. 웹 탭의 기본 오프셋(ml -15px)은 nav.ds-tab-nav하단 1px 선 위에 겹치라고 1px 더 내려간 값이에요. 헤더 탭 줄엔 그 선이 없고 overflow-y: clip이 걸려 있어 그대로 두면 2px 밑줄의 아래가 잘립니다(실측 2px → ~1.2px). .ds-app-header__tabs 안에서 1px 올려(-14px) DS가 보정해 두었으니 앱에서 다시 만지지 마세요.

배경 틴트 없음

모바일 탭에는 hover·pressed 배경 틴트를 깔지 않습니다.TabNav--ds-hover-tint-16/30으로 회색 틴트를 쓰지만, .ds-app-header__tabs 안에서는 배경이 항상 투명입니다.

터치 기기에서 :hover탭한 뒤에도 다음 탭을 누를 때까지 남습니다(iOS·Android 공통). 그래서 틴트를 그대로 두면 선택하지도 않은 탭에 회색 박스가 붙어 있는 것처럼 보여요. 모바일에서 선택 피드백은 글자색 + 하단 밑줄이 이미 담당하므로 배경은 아예 뺐습니다.

@media (hover: hover)로 가르지 않았습니다

그 방식이면 데스크톱 브라우저로 앱을 열었을 때(개발·검수 환경) 여전히 틴트가 보여 화면이 두 갈래가 됩니다. 앱 셸 탭은 어디서 열든 "배경 없음" 하나예요. 웹 탭은 그대로 틴트를 씁니다 — 이 규칙은 .ds-app-header__tabs 안쪽에만 적용됩니다.

사용법

html
<div class="contents" x-data="appHeader({
       getLeading: () => todayStr(),
       showBell: true,
       showAvatar: true,
       getAvatarInitial: () => displayName.charAt(0),
       onAvatarClick: () => switchSection('more'),
     })">
  <!-- include "../../packages/components/html/app-header.html" -->
</div>

우측에 버튼을 더하려면 getActions만 넘기면 됩니다 — partial은 손대지 않습니다.

js
getActions: () => [
  { id: 'search', icon: 'ic-search', label: '검색', onClick: openSearch },
  { id: 'filter', icon: 'ic-filter', label: '필터', onClick: openFilter,
    hidden: () => !canFilter },
]

옵션

appHeader(options)

옵션타입기본값설명
getLeading() => string""좌측 텍스트. 1depth면 보조 텍스트, 2depth면 화면 제목
onBack() => void지정하면 2depth — 좌측 백버튼 + 제목 강조 (깊이)
showBackboolean | (() => boolean)trueonBack이 있어도 false면 그 화면만 1depth. 헤더 하나가 목록·상세를 겸할 때 (깊이)
backLabelstring"뒤로"백버튼 접근성 이름
solidboolean | (() => boolean)false흰 배경 채움 (배경과 구분선). 화면마다 다르면 함수로
dividerboolean | (() => boolean)false하단 1px 선. 켜면 높이 +1px. 함수 가능
showBellboolean | (() => boolean)false공유 알림벨 렌더. 화면마다 다르면 함수로
onBellClick() => void넘기면 드롭다운을 열지 않고 이 핸들러만 호출 — 알림을 2depth 화면으로 여는 모바일용 (알림 벨)
showAvatarboolean | (() => boolean)false아바타 렌더. 화면마다 다르면 함수로
getAvatarImage() => string | null이미지 URL 오버라이드. 생략 권장 (아래 참고)
getAvatarInitial() => string스토어 사용자명 첫 글자이미지 없을 때 폴백 이니셜
onAvatarClick() => void아바타 탭
getActions() => AppHeaderAction[][]벨 앞에 놓일 아이콘 버튼들
getTabs() => AppHeaderTab[][]탭 줄 — 항목이 있으면 헤더 아래에 렌더 (탭 줄)
getActiveTab() => string""활성 탭 id
onTabSelect(id: string) => void탭 선택

AppHeaderAction

필드타입설명
idstring고유 id. aria-label 폴백이기도 함
iconstring?레지스트리 아이콘 이름
iconSvgstring?raw SVG — icon보다 우선
labelstring?접근성 이름. 없으면 id
onClick() => void클릭 핸들러
hidden() => boolean버튼을 목록에서 제거

아바타

getAvatarImage생략하면 스토어의 currentUserProfilePictureUrl을 씁니다. 이게 기본이자 권장 경로예요.

  • presigned URL이고 cache-busted 입니다
  • 사진 교체 시 refreshProfilePictureUrl() 갱신이 자동 반영됩니다
  • 이미지 404면 @error로 이니셜 폴백합니다 (PageHeader와 동일 처리)

이니셜은 getAvatarInitial을 주면 그걸 쓰고, 없으면 스토어 사용자명 첫 글자를 씁니다. 앱이 자체 표시명 규칙(displayName)을 가지면 넘겨 주세요.

알림 벨

showBell을 켜면 공유 NotificationBell이 붙고, 누르면 드롭다운이 열립니다. 데스크톱 기준 동작이에요.

모바일에서는 이 팝오버가 어차피 화면을 거의 다 덮습니다. 그래서 알림을 2depth 화면으로 여는 앱은 onBellClick을 넘기세요 — 드롭다운을 렌더하지 않고 그 핸들러만 부릅니다.

html
<div class="contents" x-data="appHeader({
       onBack: () => depthBack(),
       showBack: () => isDepth,
       showBell: () => !isDepth,
       onBellClick: () => openNotifications(),   <!-- 드롭다운 대신 2depth 진입 -->
     })">
  • 미읽음 배지는 두 모드가 공유합니다. 어느 쪽이든 같은 notificationBell 스코프 안에서 렌더되므로 카운트 폴링이 그대로 살아 있어요.
  • 여는 화면 쪽은 앱 몫입니다 — 헤더의 백버튼·제목·하단 내비 숨김은 깊이 메커니즘이 이미 처리하므로, 앱은 depth만 세우면 됩니다.
  • ⚠️ 그 depth의 주인이 셸이라면 뒤로가기를 window 이벤트로 돌리지 마세요. 섹션이 듣고 있는 app-depth-back을 쏘면 엉뚱한 섹션이 자기 뒤로가기를 실행합니다. 셸이 연 화면은 셸이 직접 닫습니다.

앱 소유로 남는 것

항목위치이유
leading 텍스트 내용앱 (todayStr() 등)앱마다 다름
아바타 탭 동작onAvatarClick라우팅은 앱 소유
액션 버튼 목록getActions화면마다 다름

관련