6.3 KiB
Frontend Design Patterns
라온누리 프로젝트의 프론트엔드 디자인 패턴 가이드입니다.
STYLE_GUIDE.md(컬러, 아이콘, 테마 규칙)와 함께, 어떻게 느끼게 할 것인가를 정의합니다.
1. 원칙
콘텐츠가 주인공이다
UI 크롬(타이틀 바, 보더, 패딩)은 최소화한다. 미디어 중심 UI(카메라, 이미지 뷰어, 크로퍼)는 풀블리드로, 컨트롤은 콘텐츠 위에 오버레이한다. 폼/설정 같은 도구 UI에서만 일반 크롬을 유지한다.
상태는 뚝 바뀌지 않는다
조건부 렌더링({ready && <UI />})은 레이아웃이 변하는 경우에만 쓴다.
그 외에는 opacity + transition으로 부드럽게 드러낸다.
// ✅ opacity 전환
<Box css={{opacity: ready ? 1 : 0, transition: "opacity 0.4s ease"}}>
// ❌ 갑자기 나타남
{ready && <Overlay />}
아이콘은 맥락에 맞게
로딩/에러 상태에 제네릭 Spinner 대신, 해당 기능의 아이콘을 사용한다.
카메라 로딩엔 LuCamera, 업로드 로딩엔 LuUpload, 에러엔 해당 아이콘의 Off 변형(LuCameraOff).
비슷한 아이콘은 나란히 두지 않는다
같은 도구 모음 안에서 시각적으로 유사한 아이콘이 인접하면 혼란을 준다. 의미가 다른 아이콘이 비슷하게 보이면 아이콘 자체를 바꾼다.
✅ 회전 LuRotateCw | 초기화 LuUndo2 — 형태가 다름
❌ 회전 LuRotateCw | 초기화 LuRefreshCw — 둘 다 회전 화살표
2. 애니메이션
모든 애니메이션은 CSS @keyframes + Chakra css prop으로 구현한다.
수치 기준
| 용도 | duration | easing |
|---|---|---|
| 등장 (fadeIn) | 0.3s | ease-out |
| 호버 전환 | 0.2s | — |
| 상태 전환 (색상, 크기) | 0.3s | — |
| 오버레이 reveal | 0.4s | ease |
| 호흡 (pulse) | 2s | ease-in-out |
등장 — scale + fade
상태 변화(에러/성공/새 요소)에 사용. 0.3초, 살짝 작게 시작.
css={{
animation: "fadeIn 0.3s ease-out",
"@keyframes fadeIn": {
from: {opacity: 0, transform: "scale(0.9)"},
to: {opacity: 1, transform: "scale(1)"},
},
}}
순차 등장이 필요하면 animation-delay를 0.1s 간격으로 준다.
호흡 — 준비 상태 표현
"사용 가능"을 은은하게 알린다. 2초 주기, 미세한 변화.
// boxShadow 펄스 (버튼 주변 링)
"@keyframes pulse": {
"0%, 100%": {boxShadow: "0 0 0 0px {colors.brand.solid/30}"},
"50%": {boxShadow: "0 0 0 6px {colors.brand.solid/0}"},
}
// opacity + scale (아이콘)
"@keyframes pulse": {
"0%, 100%": {opacity: 0.4, transform: "scale(1)"},
"50%": {opacity: 1, transform: "scale(1.1)"},
}
촉각 피드백
모든 커스텀 버튼(Box as="button")에 적용한다.
css={{
"&:hover": { /* 배경/테두리 변화 */ },
"&:active": { transform: "scale(0.9)" }, // 눌림 느낌
}}
transition="all 0.2s"
keyframe 네이밍
{컴포넌트}{동작} camelCase. 범용은 접두사 없이.
예: cameraPulse, shutterReady, fadeIn
3. 컴포넌트 패턴
Frosted Glass Pill
미디어 위에 텍스트를 올릴 때 사용. 반투명 배경 + 블러.
<Text fontSize="xs" color="white" px={3} py={1} borderRadius="full"
css={{background: "rgba(0,0,0,0.5)", backdropFilter: "blur(8px)"}}
/>
플로팅 툴바
미디어 위 도구 버튼을 frosted glass bar로 그룹화. 아이콘 전용, title로 레이블.
성격이 다른 도구 그룹 사이에는 세로 디바이더를 넣는다.
<Box display="flex" alignItems="center" gap={1} px={3} py={1.5}
borderRadius="full"
css={{background: "rgba(0,0,0,0.5)", backdropFilter: "blur(8px)"}}
>
{/* 편집 도구 */}
<ToolButton icon={<LuRotateCw />} />
<ToolButton icon={<LuZoomIn />} />
<ToolButton icon={<LuZoomOut />} />
{/* 디바이더 — 파괴적 도구 분리 */}
<Box w="1px" h={5} mx={0.5} css={{background: "rgba(255,255,255,0.3)"}} />
<ToolButton icon={<LuUndo2 />} />
</Box>
ToolButton 스펙: w={9} h={9}, borderRadius="full", hover rgba(255,255,255,0.2), active scale(0.9)
은은한 글로우
어두운 배경 위 흰색 요소에 빛 번짐. boxShadow="0 0 6px rgba(255,255,255,0.4)"
안정적 비율 유지
비동기 콘텐츠(카메라, 이미지 로딩)는 aspectRatio를 고정해 레이아웃 시프트를 방지한다.
<Box css={{aspectRatio: "4 / 3"}} bg="black">
<video style={{width: "100%", height: "100%", objectFit: "cover"}} />
</Box>
절대 위치 3단 레이아웃
컨트롤 바에서 좌/중앙/우 배치. 중앙은 자연 중앙, 좌우는 position: absolute.
<Box display="flex" justifyContent="center" position="relative">
<Box position="absolute" left={8}>{/* 보조 버튼 */}</Box>
<Box>{/* 주요 버튼 — 항상 정중앙 */}</Box>
<Box position="absolute" right={8} w={11} />{/* 균형 */}
</Box>
4. 상태 표현
색상으로 구분
| 상태 | 색상 | cursor |
|---|---|---|
| 비활성/로딩 | border.muted |
not-allowed |
| 활성/준비 | brand.solid |
pointer |
| 에러 | red.subtle / red.fg |
— |
비활성 → 활성 전환에 transition: "all 0.3s"를 적용한다.
에러 상태
관련 아이콘을 subtle 배경 원 안에 배치 + fadeIn 등장. 아래에 설명 텍스트.
<VStack gap={4} py={16}>
<Box p={4} borderRadius="full" bg="red.subtle" color="red.fg"
css={{animation: "fadeIn 0.3s ease-out", ...}}>
<LuCameraOff size={32} />
</Box>
<Text color="fg.muted" textAlign="center" fontSize="sm">
{errorMessage}
</Text>
</VStack>
5. 체크리스트
새 컴포넌트를 만들 때:
- 비동기 콘텐츠 영역에
aspectRatio고정했는가? - 로딩/에러 상태에 맥락 부합 아이콘을 사용했는가?
- 커스텀 버튼에 hover 전환 + active
scale(0.9)피드백이 있는가? - 상태 전환에
transition또는animation을 적용했는가? - 미디어 위 텍스트에 frosted glass pill을 사용했는가?
- 도구 모음에서 성격이 다른 그룹을 디바이더로 분리했는가?
- 인접 아이콘이 시각적으로 혼동되지 않는가?
© 2024 BlueNovaLab. All rights reserved.