React

Scale Feedback

커스텀 컴포넌트에 SEED의 Scale Feedback을 적용하는 방법을 알아봅니다.

SEED 컴포넌트는 눌렸을 때 요소가 살짝 줄어드는 Scale Feedback을 기본으로 제공합니다. 작은 아이콘 버튼과 뷰포트 좌우를 채우는 버튼의 눌린 느낌이 같도록 만들기 위해, 축소 배율을 요소마다 다르게 계산합니다. 직접 만든 컴포넌트에도 같은 효과를 적용할 수 있습니다.

배율이 정해지는 방식

축소 배율을 고정값으로 정하는 대신, 요소의 실제 크기에서 배율을 계산합니다.

요소폭 × 높이계산된 배율실제 축소
아이콘 버튼32 × 320.938가로 2px, 세로 2px
텍스트 버튼120 × 480.958가로 5px, 세로 2px
가로로 긴 버튼343 × 480.977가로 8px, 세로 1.1px
작은 버튼22 × 220.917가로 1.8px, 세로 1.8px

배율은 렌더된 크기에서 계산되므로, 폰트 스케일링, 화면 폭 차이, 줄바꿈 등으로 인해 요소가 예상과 다른 크기로 그려져도 별도로 값을 조정할 필요가 없습니다.

적용하기

요소의 크기를 측정하는 일은 ScaleFeedback이 담당하고, 계산된 배율을 언제 어떤 요소에 적용할지는 각 사용처에서 직접 정의합니다.

요소 크기 측정하기

축소할 요소를 ScaleFeedback으로 감쌉니다.

MyButton.tsx
import { ScaleFeedback } from "@seed-design/react";

function MyButton({ className, ...props }) {
  return (
    <ScaleFeedback>
      <button className={className} {...props} />
    </ScaleFeedback>
  );
}

ScaleFeedback은 DOM 요소를 렌더하지 않고, 하위 요소에 ref와 className을 전달합니다. 하위 요소가 이미 가진 ref와 className은 유지됩니다.

자식 요소는 하나여야 하며, 받은 ref와 className을 DOM 요소까지 전달해야 합니다. 직접 만든 컴포넌트를 자식으로 넘긴다면 Composition 문서의 조합 원칙을 함께 확인하세요.

ScaleFeedback 없이 사용하기

ScaleFeedback을 사용할 수 없는 경우 useScaleFeedback을 사용합니다. scaleFeedbackRef와 scaleFeedbackClassName을 같은 요소에 전달하세요.

MyButton.tsx
import { useScaleFeedback } from "@seed-design/react";
import clsx from "clsx";

function MyButton({ className, ...props }) {
  const { scaleFeedbackRef, scaleFeedbackClassName } = useScaleFeedback();

  return (
    <button
      ref={scaleFeedbackRef} 
      className={clsx(scaleFeedbackClassName, className)} 
      {...props}
    />
  );
}

계산된 배율 사용하기

계산된 배율은 --seed-feedback-scale, 트랜지션은 --seed-feedback-scale-transition CSS 변수로 공개됩니다.

JS로 스타일을 작성한다면 @seed-design/css/scale-feedback에서 feedbackScale, feedbackScaleTransition으로 가져올 수 있습니다.

MyButton.tsx
<ScaleFeedback>
  <button
    className={clsx(
      "bg-bg-brand-solid p-x3 rounded-r2",
      "[transition:background-color_0.2s,var(--seed-feedback-scale-transition)]", 
      "active:scale-(--seed-feedback-scale)", 
    )}
  />
</ScaleFeedback>

유틸리티 클래스 활용하기

배율을 여러 컴포넌트에 적용한다면 프로젝트에 유틸리티를 등록해두세요. active:scale-(--seed-feedback-scale) 대신 active:feedback-scale로 쓸 수 있습니다.

style.css
@import "tailwindcss";

@utility feedback-scale {
  scale: var(--seed-feedback-scale);
}
MyButton.tsx
<ScaleFeedback>
  <button
    className={clsx(
      "bg-bg-brand-solid p-x3 rounded-r2",
      "[transition:background-color_0.2s,var(--seed-feedback-scale-transition)]",
      "active:feedback-scale", 
    )}
  />
</ScaleFeedback>

@theme이 아니라 @utility에 등록하세요. @theme 항목은 :root에서 값이 확정되므로 요소가 자기 크기에서 계산한 배율이 도달하지 못합니다. 클래스는 생성되는데 축소만 일어나지 않습니다.

트랜지션은 같은 방식으로 묶을 수 없습니다. transition-* 유틸리티는 Tailwind 자신의 duration과 timing function을 함께 적용해서 --seed-feedback-scale-transition에 담긴 값을 덮어버립니다. 위처럼 arbitrary property 문법을 그대로 사용하세요.

선택자 고르기

SEED 컴포넌트는 상호작용 가능한 요소에 호버할 때 색상을 변경하고, 마우스 왼쪽 버튼을 누르고 있는 동안 Scale Feedback을 적용합니다. 일관성을 위해 커스텀 컴포넌트에서도 이 기준을 유지하는 것을 권장합니다. 자세한 내용은 Interaction States 문서를 참고하세요.

:active는 비활성 상태의 요소에도 적용됩니다. 비활성 상태를 다루는 컴포넌트라면 선택자에서 제외하세요. SEED 컴포넌트는 :disabled, [disabled], [data-disabled]를 제외하고 있습니다.

개별 scale 속성

SEED 컴포넌트는 축소를 transform: scale()이 아니라 개별 transform 속성인 scale로 적용합니다. 커스텀 컴포넌트에서도 scale을 사용하세요.

transform은 shorthand라서 한 요소에 하나의 값만 가집니다. 축소를 여기에 적으면 그 요소의 이동이나 회전을 같은 값에 함께 써야 하고 변형을 추가할 때마다 축소 값을 다시 챙겨야 합니다. 개별 scale은 translate, rotate와 독립적으로 동작해서 이런 부담이 없고 ScaleFeedback이 이미 scale을 사용하므로 축소가 한 속성에 모입니다.

개별 scale 속성은 Chrome 104, Safari 14.1, Firefox 72부터 지원됩니다. 이보다 낮은 버전에서는 축소만 나타나지 않고 나머지 스타일은 그대로 동작합니다.

두 단계를 모두 적용하면 아래와 같이 동작합니다. 크기가 다른 두 요소를 눌러 비교해 보세요.

Content Scale 적용하기

배경은 그대로 두고 안쪽 콘텐츠만 줄여야 하는 요소에는 Content Scale을 적용합니다. 어떤 요소가 여기에 해당하는지는 Scale 문서에서 확인하세요. SEED 컴포넌트 중 Content Scale을 사용하는 컴포넌트는 이 구조를 내부에서 렌더하므로 따로 적용할 필요가 없습니다.

크기 측정은 위와 같지만, 배율을 적용하는 대상이 다릅니다.

  1. 배경을 그리는 요소를 ScaleFeedback으로 측정합니다. 콘텐츠도 이 요소의 크기로 계산한 배율만큼 줄어듭니다.
  2. 눌림 선택자에서 요소에 scale을 적용하는 대신 --seed-content-scale에 배율을 선언합니다.
  3. 요소의 콘텐츠를 ContentScale로 감쌉니다.
MyRow.tsx
import { ContentScale, ScaleFeedback } from "@seed-design/react";

function MyRow({ className, children, ...props }) {
  return (
    <ScaleFeedback>
      <button className={className} {...props}>
        <ContentScale>{children}</ContentScale>
      </button>
    </ScaleFeedback>
  );
}
MyRow.tsx
<MyRow
  className={clsx(
    "flex items-center gap-x3 px-x4 py-x3",
    "active:bg-bg-transparent-pressed",
    "active:[--seed-content-scale:var(--seed-feedback-scale)]", 
  )}
/>

ContentScale은 요소의 유일한 자식으로 두세요. 요소의 display와 flex·grid 레이아웃 값(flex-direction, align-items, gap, grid-template-columns 등)을 이어받아 콘텐츠를 배치하므로, 레이아웃과 패딩은 지금처럼 요소에 작성하면 됩니다. 축소 트랜지션도 ContentScale에 포함되어 있습니다.

ContentScale은 기본으로 span을 렌더합니다. 콘텐츠를 배치하는 요소가 이미 있다면 asChild로 그 요소를 대신 사용할 수 있습니다.

<ContentScale asChild>
  <div className="my-row-content">{children}</div>
</ContentScale>

자동으로 처리되는 것

아래 두 가지는 별도로 처리할 필요가 없습니다.

  • 동작 줄이기 설정: prefers-reduced-motion: reduce 환경에서는 --seed-feedback-scale이 항상 1이 됩니다.
  • 크기를 알 수 없는 상황: JavaScript가 실행되지 않았거나 첫 측정 전이라면 --seed-feedback-scale이 1이 됩니다. 잘못된 배율이 적용되는 대신 효과만 나타나지 않습니다.

알아둘 점

중첩된 SEED 컴포넌트

커스텀 컴포넌트가 Checkmark, Radiomark, Switchmark 등을 감싸는 경우, 바깥 요소와 안쪽 요소가 함께 줄어들어 안쪽 요소가 이중으로 축소됩니다. 각 컴포넌트 문서에 끄는 방법이 안내되어 있습니다.

position: fixed 자손

scale 값이 있는 요소는 stacking context이자 position: fixed 자손의 containing block이 되므로, 이 요소 안에 position: fixed 요소가 있다면 뷰포트가 아니라 이 요소를 기준으로 배치됩니다.

ScaleFeedback이 적용된 요소와 그 안의 ContentScale은 눌리지 않은 상태에서도 scale: 1을 유지하므로, 누르는 동안 자손이 따라 움직이지는 않습니다. 다만 배치 기준 자체가 뷰포트와 달라지므로 scale을 적용한 요소 하위에 position: fixed 자손이 있다면 의도한 위치에 표시되는지 확인하세요.

프로젝트 전체에서 끄기

배율을 1로 고정하는 규칙을 @layer 없이 추가하고 SEED CSS보다 뒤에 로드하세요. 배율을 읽는 모든 곳이 이 변수 하나를 거치므로 SEED 컴포넌트와 커스텀 컴포넌트가 함께 꺼집니다.

scale-feedback-off.css
.seed-scale-feedback {
  --seed-feedback-scale: 1;
}

평상시 scale: 1은 그대로 남으므로 position: fixed 자손의 배치 기준은 달라지지 않습니다.

Last updated on

목차