GODRICH

바이브코딩 마이크로 인터랙션 9종 — 한 줄 복붙으로

마이크로 인터랙션은 클릭 한 번, 저장 한 번처럼 아주 작은 동작에 화면이 곧바로 반응해 주는 짧은 피드백입니다. 아홉 개 중 하나(04)는 서버 응답을 기다리지 않고 먼저 화면부터 바꿨다가, 실패했다고 가정하면 스스로 원래대로 되돌립니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개는 인기 순이 아니라 화면에서 실제로 겪는 순서로 놓았습니다. 누르는 순간(01), 그 결과를 알려주는 완료 신호(02~03), 서버 응답을 기다리지 않고 먼저 보여주는 갱신과 로딩(04~05), 아무것도 없거나 값이 틀렸을 때(06~07), 여러 화면을 오가고 알림이 쌓이는 상황(08~09)입니다. 아래 코드는 상태가 바뀌는 규칙만 옮겼고, 클릭까지 반응하는 온전한 버전은 내려받는 압축 파일 쪽에서 확인하십시오.

01버튼 눌림 스케일

버튼을 누르는 순간 transform: scale(1)scale(.96)으로 줄어들고 그림자도 함께 낮아져, 클릭이 실제로 눌렸다는 촉각적 확인을 줍니다. 손을 떼면 스프링 이징으로 원래 크기로 돌아옵니다.

scale(0.96)스프링 이징active 상태
.btn {
  transform: scale(1);
  transition: transform $duration $easing, box-shadow $duration $easing;
}
.btn:active {
  transform: scale(.96);
  box-shadow: $shadow-press;
}

02저장 체크 애니메이션

저장 버튼을 누르면 디스켓 모양이 scale·opacity로 사라지고, 체크 아이콘 두 획이 scaleX로 순서대로 그어지며, 원형 배지가 팝업 이징으로 부풀어 오릅니다.

clip-path 모프색상 전환0.3초
.save.is-saved .save__disk {
  animation: disk-out $duration $easing forwards;
}
.save.is-saved .save__check-bar--short {
  animation: check-short $duration $easing forwards;
  animation-delay: $duration * .35;
}
@keyframes check-short {
  to { transform: rotate(45deg) scaleX(1); }
}

03복사 완료 피드백

복사 아이콘을 누르면 겹쳐진 두 사각형이 사라지고 체크 아이콘이 나타나며, 버튼 위 툴팁이 위로 살짝 뜬 채 1.2초 머물다가 사라집니다.

아이콘 스왑툴팁 페이드1.2초 유지
.copy.is-copied .copy__sheet--front,
.copy.is-copied .copy__sheet--back {
  opacity: 0;
  transform: scale(.6);
}
@keyframes tip-float {
  0%   { opacity: 0; transform: translate(-50%, 4px); }
  15%  { opacity: 1; transform: translate(-50%, -2px); }
  85%  { opacity: 1; transform: translate(-50%, -2px); }
  100% { opacity: 0; transform: translate(-50%, -10px); }
}

04낙관적 좋아요(즉시+되돌림)

하트를 누르면 서버 응답을 기다리지 않고 즉시 채워지고 숫자가 오도미터처럼 한 칸 올라갑니다. 실패로 가정한 흐름에서는 0.8초 뒤 두 값 모두 원래대로 돌아갑니다.

낙관적 업데이트롤백 애니메이션0.8초
.like__count { position: relative; height: 1em; overflow: hidden; }
.like__count-track {
  display: flex;
  flex-direction: column;
  transition: transform $duration $easing;
}
.like.is-liked .like__heart--fill { opacity: 1; transform: scale(1); }
.like.is-liked .like__count-track { transform: translateY(-50%); }

05스켈레톤→콘텐츠 크로스페이드

같은 자리에 겹쳐 둔 스켈레톤 레이어와 실제 콘텐츠 레이어가 opacity로 교차합니다. 콘텐츠 레이어는 scale(.98→1)도 같이 줘서 자리 잡는 느낌을 살짝 더합니다.

opacity 크로스페이드겹친 레이어0.4초
.card { position: relative; height: clamp(110px, 40vw, 130px); }
.card__layer {
  position: absolute; inset: 0;
  transition: opacity $duration $easing, transform $duration $easing;
}
.card__layer--skeleton { opacity: 1; }
.card__layer--content  { opacity: 0; transform: scale(.98); }

06빈 상태 아이콘 바운스

목록이 비었을 때 뜨는 트레이 아이콘이 translateY로 위아래를 부드럽게 오갑니다. 텍스트는 움직이지 않고 아이콘만 반복해서 시선을 붙잡습니다.

translateY 바운스ease-in-out무한 반복
.empty.is-demo .empty__icon {
  animation: bounce $duration $easing infinite;
}
@keyframes bounce {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-10px); }
}

07입력 오류 흔들림

잘못된 값을 제출하면 입력창을 감싼 프레임이 좌우로 짧게 흔들리고, 겹쳐 둔 빨간 테두리 링이 opacity로 나타나며, 아래 에러 문구가 위로 살짝 뜨며 표시됩니다.

translateX 흔들림빨강 테두리0.4초
.field__ring {
  position: absolute; inset: 0;
  border: 2.5px solid $error;
  opacity: 0;
}
.field.is-error .field__frame { animation: shake $duration $easing both; }
.field.is-error .field__ring  { opacity: 1; }
@keyframes shake {
  20%, 80% { transform: translateX($amount); }
  30%, 70% { transform: translateX(-$amount); }
  50%      { transform: translateX(-$amount * .7); }
}

08탭 인디케이터 슬라이드

탭 세 칸을 grid-template-columns: repeat(3, 1fr)로 똑같은 폭으로 맞춰 두면, 인디케이터는 고정 폭에서 translateX를 100% 배수로만 옮기면 됩니다. 폭 자체를 다시 재는 스크립트가 필요 없습니다.

transform 슬라이드width 보간0.25초
.tabs {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}
.tabs__indicator {
  width: calc((100% - 8px) / 3);
  transform: translateX(calc(var(--i, 0) * 100%));
  transition: transform $duration $easing;
}

09토스트 쌓임

토스트 세 개가 순서대로 아래에서 올라와 쌓입니다. 새 토스트가 뜰 때마다 기존 토스트는 고정된 슬롯 높이만큼 translateY로 밀려 올라가며 scale도 함께 줄어듭니다.

translateY 밀림scale 축소순차 스택
.toasts__list.is-demo .toast--1 {
  animation: slot-1 $duration $easing infinite;
}
@keyframes slot-1 {
  12%, 38% { opacity: 1;   transform: translateY(0) scale(1); }
  50%, 68% { opacity: .88; transform: translateY(-42px) scale(.96); }
  80%, 92% { opacity: .7;  transform: translateY(-84px) scale(.92); }
}

어디서 깨지는가 — 함정 하나

05의 스켈레톤·콘텐츠 크로스페이드는 두 레이어를 position: absolute로 겹쳐 두는 방식이라, 부모 .cardheight를 직접 정해 주지 않으면 자식이 없는 것처럼 계산돼 카드가 0으로 접힙니다. absolute 요소는 문서 흐름에서 빠지므로 부모 높이에 기여하지 못한다는 점을 MDN의 CSS 위치 지정 레이아웃 문서가 설명합니다. 09의 토스트 스택도 비슷한 값을 미리 정해 두는 방식이라, 슬롯 높이(42px)를 코드에 고정해 뒀습니다. 언어팩을 입혀 글자가 길어지는 en·ja 판에서는 문구가 이 높이를 넘지 않도록 clamp()로 글자 크기를 줄여 확인했고, 이 값이 반영된 아홉 개 파일의 압축 비밀번호는 dawuywyc이고, 아래 버튼으로 받을 수 있습니다.

02와 03의 체크 아이콘은 SVG 대신 border와 회전한 div로 그렸습니다. 아이콘 크기를 바꾸면 rotate(-52deg)처럼 손으로 맞춘 각도도 다시 계산해야 하는데, 이건 SVG path를 쓰면 안 생기는 수고입니다. 크기를 자주 바꿀 아이콘이라면 처음부터 SVG로 그리는 편이 낫습니다.

접근성

아홉 개 전부 prefers-reduced-motion: reduce에서 반복 애니메이션을 끕니다. 다만 상태 전환 자체는 지우지 않는 게 원칙이라, 04의 좋아요 표시나 07의 에러 링처럼 값이 바뀌었다는 사실은 트랜지션 없이 즉시 남습니다. 07의 입력 필드는 role="alert"로 에러 문구를 스크린 리더에 바로 읽어 주고, 04의 좋아요 버튼은 aria-pressed로 지금 상태를 알립니다. 08의 탭은 role="tab"aria-selected를 함께 씁니다. 자세한 값 조합은 MDN의 @media prefers-reduced-motion 문서에 정리돼 있습니다.

@media (prefers-reduced-motion: reduce) {
  .field__ring, .field__msg { transition: none; }
  .field.is-demo .field__frame { animation: none !important; }
}

다른 CSS 애니메이션 주제는 CSS 애니메이션 카테고리에 모여 있고, 이 사이트가 무엇을 다루는지는 소개에 있습니다.

FAQ

마이크로 인터랙션과 일반 CSS 애니메이션은 뭐가 다릅니까?

마이크로 인터랙션은 배너 등장처럼 장식이 아니라 클릭·저장·오류처럼 사용자가 한 행동에 대한 응답입니다. 그래서 지속 시간이 짧고(220~600ms), 실제 상태 변화와 묶여 있어야 합니다. 이 글의 아홉 개는 전부 실제 클릭이나 입력에 반응하도록 짠 이유가 여기 있습니다.

04의 낙관적 업데이트는 실제 서버 요청과 어떻게 연결합니까?

React 판의 onToggle 같은 콜백에 실제 요청을 넣고, 성공하면 true를 돌려주면 됩니다. 실패하거나 콜백이 없으면 정해 둔 시간(기본 800ms) 뒤 화면이 자동으로 원래 값으로 돌아갑니다. zip의 react/ 폴더에 전체 로직이 있습니다.

아홉 개를 전부 한 페이지에 같이 써도 됩니까?

네, 흔한 조합입니다. 다만 08처럼 .is-demo 클래스로 자동 반복시켜 둔 요소는 실제 화면에서는 사용자의 진짜 상태(클릭·오류 여부)로만 움직이게 그 클래스를 지우고 시작해야 합니다. zip의 vanilla/ 폴더 각 파일 안 스크립트가 그 처리를 그대로 담고 있습니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.