참고 사이트의 스타일을 분석해 Aster 랜딩 페이지 리뉴얼하기

X Facebook

Written by

in

Aster 랜딩 페이지를 리뉴얼하려고 참고 사이트를 골랐다. 스크린샷을 옆에 띄우고 배경색부터 맞췄다. #0a0a0a와 #0d0d0d를 번갈아 넣다가 비슷해 보이는 값에서 멈췄지만, 실제 CSS 값과는 달랐다.

눈으로 맞추는 대신 브라우저에서 적용된 스타일을 읽었다. 색상과 타이포그래피를 확인해 Aster에 적용한 과정, 콘텐츠에 맞게 다시 만든 이미지, 헤더와 제목 바를 구현하며 겪은 문제를 정리한다.

1. 스크린샷의 픽셀 색상과 CSS 색상값이 다른 이유

스포이드로 찍은 값은 그 자리의 합성 결과다. 스크린샷이 PNG 면 픽셀 자체는 무손실로 남지만, 그 픽셀이 CSS에 적힌 선언값이라는 보장은 없다. 반투명 면이 겹쳐 있거나 글자 가장자리에 걸리면 섞인 값이 나온다.

무엇보다 사이트는 색 하나로 되어 있지 않다.

배경과 카드 면이 한 단 차이로 놓이면 그 차이가 층을 만든다. 스포이드는 두 값을 따로 찍을 뿐이다. 둘이 몇 단 떨어져 있는지는 알려 주지 않는다. 그래서 하나씩 맞추면 각각은 비슷한데 전체가 안 맞는다.

값은 이미 브라우저 안에 있다. 렌더된 화면에서 그대로 읽으면 된다.

2. getComputedStyle로 참고 사이트의 색상과 타이포그래피 확인하기

콘솔에서 getComputedStyle로 읽었다.

getComputedStyle(document.body).backgroundColor  // "rgb(8, 9, 10)"
getComputedStyle(h1).fontWeight                  // "510"
getComputedStyle(h1).letterSpacing               // "-1.408px"

읽어 온 값이다.

항목 값
배경 rgb(8, 9, 10)
전경 rgb(247,248,248)
카드 면 rgb(15, 16, 17)
선 rgb(35, 37, 42)
muted rgb(138,143,152)
버튼 면 rgb(229,229,230)
반경 16px

표 1. 참고 사이트의 렌더 화면에서 읽은 값

두 가지가 눈으로는 안 보이던 것이다.

첫째, 카드 면이 배경보다 밝다. #0f1011과 #08090a는 스크린샷에서 둘 다 검정으로 보인다. 이 한 단 차이가 카드를 면으로 세운다. 라이트 모드에서는 관계를 뒤집어 카드를 배경보다 한 단 어둡게 뒀다.

둘째, 헤딩 굵기가 510이다. Tailwind의 font-medium은 500이고 font-semibold는 600이다. 510은 그 사이라 이름 붙은 단계에는 없다. 임의 값 유틸리티로 적으면 되고, 가변 폰트라 그 값이 실제로 나온다.

className="font-[510]"

자간도 같다. -1.408px은 64px 기준으로 -0.022em이다. 본문 쪽 -0.13px은 13px 기준 -0.01em이다. 눈으로 “좀 좁다”까지는 보이지만 얼마나 좁은지는 안 보인다.

3. 흰 배경에서 브랜드 색상의 대비 높이기

쓰던 중립 스케일은 teal 색조를 띠고 있었다. oklch(0.1672 0.0082 181.8)처럼 채도가 조금 섞인 회색이다. 그 위에 브랜드 teal을 올리면 액센트가 액센트로 안 읽힌다. 배경이 이미 같은 계열이기 때문이다.

중립을 완전 중립으로 내렸다. 그랬더니 라이트 배경이 흰색이 되고 그 위에서 브랜드 코랄이 미달로 드러났다.

ratio('#ff6b4a', '#ffffff')  // 2.82

워드마크 aster의 a 한 글자가 이 색이다.

WCAG는 로고와 브랜드명을 대비 요구에서 빼 준다. 그러니 이 글자는 기준 미달이 아니다. 그래도 3:1을 자체 목표로 걸었다. 읽히라고 크게 쓴 글자인데 배경에 묻히면 브랜드 쪽이 손해다.

이전 배경은 사진이었다. 자리마다 배경값이 달라 단일 쌍으로는 못 쟀고, 그때는 글자가 놓이는 구역의 미달 픽셀 비율로 판정했다. 배경이 색이 되면서 쌍으로 잴 수 있게 됐다.

값 흰 배경 대비
#ff6b4a 2.82
#f2542d 3.45
#e8482a 3.89
#dd4426 4.26

표 2. 코랄 후보와 흰 배경 대비

#f2542d를 골랐다. 3:1을 넘는 값 중 원색에 가장 가깝다. 다크의 #ff6b4a는 7.07이라 그대로 뒀다. 두 테마의 코랄은 이것 하나 때문에 다르다.

전체를 다시 쟀다. 본문·보조 글자·카드 안 글자·주 버튼·링크 hover를 각각 배경과 카드 면에 대고, 라이트와 다크 양쪽으로 10쌍이다. 본문 기준은 AA 4.5이고 워드마크만 large 3:1이다. 미달 0건이다. 다크 배경은 #08090a이고 거기서 코랄은 7.07이다.

4. Aster 콘텐츠에 맞게 히어로 이미지와 영역별 일러스트 바꾸기

히어로 아래에는 브랜드 사진이 한 장 들어가 있었다. teal 종이 조각을 찍은 사진이다. 배경이 중립으로 내려가자 그 사진만 혼자 밝게 떠올랐다.

톤을 낮추는 방법도 있었다. 그런데 참고한 사이트는 그 자리에 사진을 두지 않는다. 제품 화면을 둔다. 그 자리는 “이 도구가 어떻게 생겼는가”를 보여 주는 자리다.

이 사이트의 제품은 글이다. 그래서 그 자리에 케이스 목록 화면을 그려 넣었다. 축소한 상단 바, Cases 라벨과 건수, 카드 여덟 장이다. 카드는 여섯 장으로 시작했는데 프레임 아래가 비었다. 여덟 장으로 늘리니 마지막 줄이 아래 페이드에 걸려 목록이 화면 밖으로 이어지는 것처럼 보인다.

같은 판단이 세 영역을 설명하는 절에서 한 번 더 나왔다. 참고 사이트는 거기에 아이소메트릭 선화를 세 장 둔다. 형식만 가져오면 그 사이트의 그림이 된다. 그래서 형식은 두고 그리는 대상을 바꿨다.

그림 무엇을 그리나
FIG 0.1 흩어진 작은 조각. 아직 아무것도 안 이뤘다
FIG 0.2 기둥과 보만 선 골조. 면은 아직 없다
FIG 0.3 면이 채워진 덩어리. 윗면에 홈 하나

표 3. 세 영역에 대응하는 도면 세 장

세 장이 실험에서 정리를 거쳐 제품으로 가는 순서다. 중심선과 치수선과 숨은선 셋이 도면으로 읽히게 한다.

여기서 걸린 것이 좌표였다. 바닥 격자를 중심에서 좌우로 밀어 그리려고 t를 ±0.34로 두고 이렇게 적었다.

// ❌ t 가 음수면 x2 가 cx + w 보다 커진다 — 오른쪽 꼭짓점 밖이다
<line x1={cx + w * t} y1={base - h * t} x2={cx + w - w * t} y2={base + h * t} />

t = -0.34, cx = 120, w = 68을 넣으면 x2가 211.1이다. 마름모의 오른쪽 끝은 188이다.

중심에서 미는 대신 마주 보는 두 변 위의 같은 비율 지점을 잇는다. t는 0과 1 사이다.

// ✅ 두 끝점이 모두 변 위에 있으니 선분은 안쪽에 들어온다
<line x1={cx + w * t} y1={base - h + h * t} x2={cx - w + w * t} y2={base + h * t} />

5. CSS 변수로 헤더 높이와 제목 바 위치 맞추기

상세 화면에서 글 제목을 헤더 아래에 걸기로 했다. 그러려면 제목 바가 헤더 높이를 알아야 한다.

높이를 61px로 적어 두면 헤더 쪽을 고칠 때 같이 안 고쳐진다. 그래서 토큰으로 뺐다.

--header-height: 3.8125rem; /* 61px */

핵심은 헤더가 이 값을 자기 높이로 쓴다는 것이다.

<header class="sticky top-0 h-[var(--header-height)]">

한쪽은 높이로 쓰고 다른 쪽은 위치로 쓴다. 같은 변수를 보고 있으니 둘이 어긋날 수가 없다.

높이는 두 단이다. 제목 바가 걸리면 헤더가 한 단 줄고, 그만큼을 바가 받는다.

--header-height: 3.8125rem;         /* 61px — 평소 */
--header-height-compact: 3rem;      /* 48px — 제목 바가 걸렸을 때 */

6. 제목 바 구현 중 발생한 rootMargin 오류와 애니메이션 문제 해결하기

제목이 화면 위로 지나갔는지는 IntersectionObserver가 판정한다. 눈금 하나를 제목 블록 끝에 두고 그 눈금이 헤더 뒤로 사라지면 바를 띄운다.

IntersectionObserver의 rootMargin에 rem을 넣어 발생한 오류

눈금이 헤더에 가려지는 구간을 빼야 하니 rootMargin에 헤더 높이를 넣었다. 토큰이 이미 있으니 그대로 읽어 썼다.

const h = getComputedStyle(document.documentElement)
  .getPropertyValue('--header-height');   // "3.8125rem"

new IntersectionObserver(cb, { rootMargin: `-${h} 0px 0px 0px` });

화면이 에러로 덮였다.

Failed to construct 'IntersectionObserver':
rootMargin must be specified in pixels or percent.

rootMargin은 CSS 길이를 다 받지 않는다. px과 %만 받는다. rem은 루트 글자 크기를 알아야 픽셀이 되는데, 옵저버를 만드는 시점에 그 환산을 하지 않는다. CSS 변수는 CSS 안에서만 길이다. 밖으로 꺼내면 그냥 "3.8125rem"이라는 문자열이다.

헤더에서 직접 쟀다.

const offset = Math.round(header.getBoundingClientRect().height); // 61

Tailwind v4에서 transform 전환이 translate에 적용되지 않은 이유

바가 나타날 때 살짝 내려오게 하려고 이렇게 적었다.

className="transition-[opacity,transform] -translate-y-1 opacity-0"

투명도는 부드러운데 위치는 튀었다. 계산값을 찍어 봤다.

getComputedStyle(bar).transform          // "none"
getComputedStyle(bar).translate          // "0px -4px"
getComputedStyle(bar).transitionProperty // "opacity, transform"

transform이 none이다. 옮겨진 값은 translate 라는 다른 속성에 들어가 있었다. translate·rotate·scale은 2022년에 들어온 독립 속성이다. 그 전에는 셋을 transform 한 칸에 문자열로 쌓아야 했고 두 군데서 각각 건드리면 뒤엣것이 앞엣것을 지웠다. 칸을 셋으로 나눈 이유가 그것이다.

Tailwind v4의 -translate-y-1은 새 칸 쪽을 쓴다. 그래서 transition에 transform을 적으면 감시 대상이 빈 칸이다. 값은 바뀌는데 전환할 속성이 아니라서 즉시 적용된다. v3에서는 같은 클래스가 --tw-translate-y를 거쳐 transform을 만들었다. 클래스 이름은 그대로인데 어느 속성에 들어가는지가 달라졌다.

7. 헤더와 제목 바의 테두리가 겹쳐 보이는 문제 수정하기

제목 바는 투명도로 켜지지 않는다. 헤더 뒤에 숨어 있다가 아래로 밀려 나온다. 헤더 바로 아래에 붙여 두고 평소에는 자기 높이만큼 위로 올려 헤더와 겹쳐 두면 된다. 헤더가 불투명하니 그동안은 안 보인다.

<div className={stuck ? "translate-y-0" : "-translate-y-full"}>

맨 위에서 헤더 아래 선이 두 줄로 보였다. 제목 바도 아래에 1px 선이 있다. 딱 100% 만 올리면 바의 아래 끝이 헤더의 아래 끝과 같은 자리에 온다.

같은 자리인데 왜 두 줄로 보이나. 바의 높이가 정수가 아니어서다. 세로 여백 10px 둘에 13px 글자의 줄 높이 19.5px이 붙어 소수점이 남는다. 서브픽셀 좌표는 직접 재지 않았다. 화면에 2px 짜리 흐린 띠가 보인 것이 관찰한 사실이고, 소수점 높이가 원인이라는 것은 그 값들에서 짐작한 것이다.

1px을 더 올려 헤더 선 뒤로 완전히 밀어 넣었다.

"-translate-y-[calc(100%+1px)]"

잰 값으로 확인했다. 바의 아래 끝이 60, 헤더의 아래 끝이 61이다.

이 자리에서 한 가지를 더 정했다. 나올 때와 들어갈 때의 속도를 다르게 뒀다. 나올 때 200ms, 들어갈 때 380ms 다. 헤더의 높이 전환은 양쪽 다 200ms 라, 되돌아갈 때는 헤더가 먼저 제 높이를 찾고 그 뒤로 바가 마저 올라간다. 같은 속도로 두면 둘이 한꺼번에 사라져 무엇이 무엇을 밀어낸 건지 안 보인다.

스타일을 가져오며 생긴 한계와 헤더 구현의 유지보수 부담

실측으로 값을 가져오면 왜 그 값인지는 안 따라온다. 510이라는 굵기가 왜 500도 600도 아닌지, 그 결정의 근거는 화면에 안 찍혀 있다. 값만 복사하면 나중에 그 값을 바꿔야 할 때 기준이 없다. 그래서 읽은 값과 함께 어디서 어떻게 읽었는지를 토큰 문서에 적어 뒀다.

중립을 완전 중립으로 내린 대가도 있다. 기존 회색에 섞여 있던 teal이 브랜드와 배경을 묶어 주고 있었다. 그 연결이 끊기면서 색은 브랜드 둘만 남았고 화면은 그만큼 차가워졌다. 액센트가 또렷해진 것과 맞바꾼 것이다.

제목 바가 헤더 뒤에서 나오려면 헤더보다 뒤에 그려져야 한다. 둘은 같은 sticky 래퍼의 형제이고 z-index가 헤더 10, 바 0이다.

헤더는 z-index를 가진 sticky 요소라 자기 스태킹 컨텍스트를 만든다. 그래서 헤더 안쪽에 무엇을 넣든 바깥 순서는 안 뒤집힌다. 대신 헤더나 바 자신의 z 값이 이 동작 전체를 지탱한다. 둘 중 하나를 나중에 고치면 바가 헤더 앞으로 나온다. 화면의 모양이 클래스 두 개에 매달려 있다.

반응형 확인 결과와 남은 빌드·실기기 검증

창 크기 조절 도구가 먹지 않아 좁은 폭은 같은 origin의 iframe 안에서 쟀다. iframe 안에서는 미디어쿼리가 iframe 폭으로 평가되므로 레이아웃 값은 그대로 믿을 수 있다. 375px에서 scrollWidth 360, 768px에서 753, 넘치는 요소 0개다.

그런데 IntersectionObserver 판정은 그 방법으로 확인하지 못했다. iframe을 화면 밖에 두면 안쪽 요소가 교차하지 않는 것으로 잡힌다. 제목 바 동작은 실제 탭에서 따로 확인했다.

헤더를 클라이언트 컴포넌트로 바꾼 뒤로는 프로덕션 빌드를 아직 안 돌렸다. 개발 서버에서는 도는 것을 확인했다.

디바이스에서도 아직 안 봤다. iOS 사파리의 주소 표시줄이 접힐 때 sticky 헤더와 그 아래 붙는 바가 어떻게 맞물리는지는 확인 못 한 상태다.

리뉴얼에 적용한 스타일 값과 직접 구현한 UI 정리

색 일곱 개와 굵기 하나, 자간 둘이다. 눈으로 고르던 값을 브라우저에서 읽어 왔을 뿐이고 그 과정에서 스크린샷으로는 안 보이던 것 두 개를 알았다. 카드 면이 배경보다 밝다는 것과, 헤딩 굵기가 이름 붙은 단계에 없는 값이라는 것이다.

값이 아닌 것은 가져오지 못했다. 사진이 놓이던 자리에는 이 사이트의 화면을 그렸고 선화 세 장은 형식만 두고 그리는 대상을 바꿨다. 그림은 읽어 올 수 있는 종류가 아니었다.

읽어 온 값도 CSS 밖에서는 문자열이다. rootMargin이 거부했고 결국 헤더에서 직접 쟀다. 전환할 속성 이름도 마찬가지였다. transform이라고 적어 둔 자리에 실제로 들어 있던 것은 translate 였다.

Comments

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다