B-7: 애니메이션 & 전환
안녕하세요, 홍순구 튜터입니다. 그동안 우리는 JavaScript로 화면에 생명을 불어넣는 긴 여정을 지나왔어요. 좋아요를 누르면 하트가 켜지고, 댓글이 달리고, 스크롤하면 다음 게시물이 줄줄이 따라오고, 로그인 정보가 저장되고요. 정적이던 인스타그램이 진짜 앱처럼 움직이기 시작했죠.
그런데 한 가지, 아직 어딘가 뻣뻣한 구석이 있어요. 좋아요를 누르면 하트가 툭 하고 즉시 빨개져요. 부드럽게 통! 하고 튀어오르는 맛이 없죠. 더보기 메뉴를 열면 팍 하고 갑자기 나타나고요. 진짜 인스타그램은 하트가 통통 튀고, 메뉴가 스르륵 떠오르고, 페이지가 끊기지 않고 부드럽게 넘어가잖아요. 그 "부드러움"의 정체가 바로 오늘 배울 애니메이션(animation)과 전환(transition)이에요.
기억하시나요? CSS를 마지막으로 만진 B-6 반응형 수업 끝에, 제가 이렇게 예고했어요. "화면이 멈춰 있다 — 다음엔 좋아요 하트가 통통 튀고, 메뉴가 스르륵 열리게 만들어요"라고요. 오늘이 바로 그 약속을 지키는 시간이에요.
재밌는 건, 오늘 배울 건 전부 CSS만으로 한다는 거예요. 움직임이라고 하면 JavaScript를 떠올리기 쉬운데, 사실 화면의 부드러운 변화 대부분은 CSS가 더 잘하고 더 가벼워요. CSS는 "정지 화면의 모습"만 정하는 줄 알았죠? 오늘은 거기에 시간(time)이라는 축을 하나 더 얹어볼 거예요.
비유를 들어볼게요. 지금까지 우리가 그린 화면은 한 장의 사진이었어요. "이 버튼은 파란색, 이 카드는 여기"처럼 한순간의 모습만 정했죠. 오늘부터는 사진이 아니라 짧은 영상을 만들어요. "버튼이 0.2초 동안 천천히 색이 변한다", "하트가 0.4초 동안 통 튀어오른다"처럼, 시작과 끝 사이의 시간을 우리가 연출하는 거예요.
지금까지 (사진 한 장) 오늘부터 (짧은 영상)
hover 하면 hover 하면
┌─────┐ ┌─────┐ ┌─────┐ → ┌─────┐ → ┌─────┐
│ 파랑│ → │ 빨강│ (0초, 툭) │ 파랑│ │ 보라│ │ 빨강│ (0.2초, 스르륵)
└─────┘ └─────┘ └─────┘ └─────┘ └─────┘
순간 점프 시간을 들여 부드럽게
💡 오늘 수업의 핵심 — "transition으로 상태 변화에 시간을 입히고, transform·@keyframes로 움직임을 만들고, @starting-style·View Transitions로 모달과 페이지 전환까지 부드럽게 만든다" 🎯
🎯 학습 목표
- 정적인 CSS에 "시간"을 더하는
transition의 4요소(속성·시간·가속도·지연)를 이해합니다. - 무엇은 부드럽게 변할 수 있고 무엇은 안 되는지(보간 가능 속성), 그리고
timing-function으로 변화의 속도 곡선을 조절합니다. transform(translate/rotate/scale)으로 요소를 움직이고, 그것이 왜 레이아웃을 흔들지 않는지 이해합니다.:hover/:active/:focus-visible세 상태로 사용자 행동에 반응하는 인터랙션을 디자인합니다.@keyframes로 여러 구간을 거치는 애니메이션을 만들어, 좋아요 하트가 통! 튀는 효과를 완성합니다.@starting-style로 모달(다이얼로그)과 더보기 메뉴(팝오버)가 스르륵 등장하게 만듭니다.- View Transitions API로 페이지 사이를 끊김 없이 부드럽게 넘기고, 브라우저 호환을 고려합니다.
prefers-reduced-motion으로 움직임에 민감한 사용자를 배려하는 접근성 가드를 답니다.
오늘도 외우려 하지 마세요. 애니메이션은 눈으로 보는 게 전부예요. 코드의 숫자(0.2s를 2s로) 하나만 바꿔도 변화가 눈에 확 들어와요. 브라우저를 열어두고, 값을 바꿔가며 직접 통통 튀어보는 게 표로 외우는 것보다 백배 빨라요. 자, 멈춰 있던 화면에 시간을 흘려볼까요?
Step 1: "정적이던 화면에 시간이 흐른다 — transition 첫걸음"
먼저 "전환"이 없을 때 무슨 일이 일어나는지부터 봐요. 우리 추천 사이드바의 "팔로우" 글자에 마우스를 올리면 색이 바뀌게 해볼게요. CSS는 이미 알죠? :hover로요.
/* instagram-clone-frontend/css/components.css */
.suggest-follow:hover {
color: var(--ig-blue-strong);
}
마우스를 올리는 순간, 글자색이 툭 하고 즉시 진한 파랑으로 바뀌어요. 0초 만에요. 떼면 또 툭 하고 돌아오고요. 기능은 멀쩡하지만, 어딘가 딱딱하죠. 진짜 앱은 이 변화가 아주 짧게라도 "스르륵" 흘러요.
여기서 등장하는 게 **transition(전환)**이에요. 우리말로 풀면 "한 상태에서 다른 상태로 시간을 들여 넘어가게 하라"예요. 딱 한 줄 추가해볼게요.
.suggest-follow {
transition: color 0.2s ease;
}
.suggest-follow:hover {
color: var(--ig-blue-strong);
}
이제 마우스를 올리면 글자색이 0.2초에 걸쳐 부드럽게 파랑으로 물들어요. 떼면 또 0.2초에 걸쳐 천천히 돌아오고요. 딱 한 줄 추가했을 뿐인데 느낌이 확 달라지죠.
여기서 중요한 포인트 하나. transition은 평소 상태(.suggest-follow)에 적어요. :hover에 적는 게 아니라요. 왜냐하면 transition은 "이 요소는 변화가 생기면 0.2초 들여 넘어가는 성격이야"라는 그 요소의 성격을 정하는 거거든요. 올라갈 때도, 내려올 때도 그 성격이 적용돼야 하니까 평소 상태에 적는 거예요.
transition의 네 가지 요소
transition: color 0.2s ease 이 한 줄을 뜯어보면 네 가지를 적을 수 있어요.
transition: color 0.2s ease 0s
───── ──── ──── ──
①속성 ②시간 ③곡선 ④지연
- ① 속성(property) — 무엇을 부드럽게 할지. 여기선
color(글자색). - ② 지속 시간(duration) — 얼마 동안 변할지.
0.2s는 0.2초.200ms로 적어도 같아요. - ③ 타이밍 함수(timing-function) — 변화의 "속도 곡선".
ease는 "처음엔 빠르게 시작했다 끝에서 천천히"예요. 가장 자연스러워서 기본값이에요. - ④ 지연(delay) — 몇 초 뒤에 시작할지. 안 적으면
0s(바로 시작). 거의 생략해요.
보통은 transition: 속성 시간 곡선 세 개만 적어요. 지연은 특별한 경우에만 쓰고요.
🙋 학생 질문 — "튜터님, 0.2초면 너무 짧지 않아요? 눈에 보이긴 하나요?"
좋은 질문이에요. 0.2초는 사람 눈에 "부드럽다"고 느껴지는 가장 자연스러운 길이예요. 너무 짧으면(0.05초) 그냥 툭 바뀌는 것과 구분이 안 되고, 너무 길면(1초 이상) 답답해서 "왜 이렇게 굼떠?" 싶어져요.
직접 확인해보세요. 0.2s를 2s로 바꿔서 저장하고 마우스를 올려보세요. 색이 2초에 걸쳐 느릿느릿 변하는 게 또렷이 보일 거예요. 반대로 0.05s로 줄이면 거의 즉시 바뀌는 것처럼 보이고요. 이렇게 눈으로 직접 비교해보면 "왜 0.2초쯤이 적당한가"가 몸에 익어요. 실무에서는 대개 0.15초~0.3초 사이를 써요.
자, 우리 코드베이스에는 팔로우 버튼 말고도 색이 부드럽게 변해야 할 글자 버튼이 여럿 있어요. 추천의 "전환", 가입 링크, Facebook 로그인 같은 것들이요. 이들을 한꺼번에 묶어서 같은 전환을 줬어요.
.suggest-follow,
.me-switch,
.signup-link,
.fb-login {
transition: color 0.2s ease;
}
이렇게 셀렉터를 쉼표로 묶으면 네 개 버튼이 모두 "색이 0.2초 들여 변하는 성격"을 갖게 돼요. 작은 한 줄이지만, 사이트 전체가 한결 부드러워지는 첫걸음이에요.
Step 2: "무엇을, 어떻게 부드럽게 — transition을 더 깊이"
Step 1에서 색(color)을 부드럽게 바꿔봤어요. 그럼 아무거나 다 부드럽게 바뀔까요? 여기서 중요한 원리를 하나 짚어야 해요.
부드럽게 변할 수 있는 것 vs 없는 것
transition은 시작값과 끝값 사이를 자동으로 채워서 부드럽게 만들어요. 이걸 "보간(interpolation)"이라고 해요. 빈칸을 중간값으로 메운다는 뜻이에요. 그러려면 시작과 끝이 숫자로 이어진 사이값을 가질 수 있어야 해요.
부드럽게 변할 수 있음 (사이값이 있음)
─────────────────────────────────
color: 파랑 ──→ 보라 ──→ 빨강 (색은 중간색이 있음)
opacity: 0 ──→ 0.3 ──→ 0.7 ──→ 1 (투명도는 중간값이 있음)
width: 100px ──→ 150px ──→ 200px (크기는 중간값이 있음)
transform: 작게 ──→ 중간 ──→ 크게 (변형도 중간값이 있음)
부드럽게 변할 수 없음 (중간이 없음, 딱 끊김)
─────────────────────────────────
display: none ──✗──→ block (보임/안보임 사이에 중간이 없음)
color, opacity(투명도), width, transform 같은 건 중간값이 있어서 부드럽게 변해요. 하지만 display: none(안 보임)과 display: block(보임) 사이에는 "반쯤 보임" 같은 중간이 없어요. 그래서 display는 transition이 안 먹고 툭 끊겨요.
이게 왜 중요하냐면, 모달이나 메뉴를 "스르륵" 열려면 결국 display를 다뤄야 하거든요. 이 "display는 부드럽게 안 된다"는 문제를 어떻게 푸는지는 Step 6에서 해결할 거예요. 지금은 "아, display는 원래 transition이 안 되는구나" 정도만 기억해두세요.
타이밍 함수 — 변화의 속도 곡선
Step 1에서 ease를 썼죠. 이건 변화가 일정한 속도가 아니라 "곡선"을 그린다는 뜻이에요. 종류를 볼게요.
linear ┃ ●────────────● 처음부터 끝까지 같은 속도 (기계적)
ease ┃ ●─────────● 빠르게 시작 → 끝에서 감속 (가장 자연스러움)
ease-in ┃ ●●────────● 느리게 시작 → 점점 빨라짐
ease-out ┃ ●────────●● 빠르게 시작 → 끝에서 천천히
cubic-bezier ┃ 내가 곡선을 직접 그림 (자유 곡선)
linear— 처음부터 끝까지 같은 속도. 로딩 스피너처럼 일정하게 도는 것에 어울려요(실제로 우리 스피너가linear예요).ease— 빠르게 출발했다 끝에서 부드럽게 멈춤. 대부분의 상황에 가장 자연스러워서 기본값.ease-in/ease-out— 가속만, 감속만.cubic-bezier(...)— 네 개의 숫자로 곡선을 직접 정의해요. 통통 튀는 느낌까지 만들 수 있어요(과제에서 실험해볼게요).
all은 조심해서
transition에는 속성 자리에 all을 적을 수도 있어요. "모든 속성의 변화를 부드럽게"라는 뜻이죠.
/* ⚠️ 편하지만 권장하지 않는 방식 */
.btn-edit {
transition: all 0.2s ease;
}
편해 보이지만, 이건 권장하지 않아요. 의도하지 않은 속성까지 전부 부드럽게 만들어서, 가끔 이상한 곳에서 예기치 않은 전환이 보이거나 성능에 부담을 줄 수 있거든요. 그래서 실무에서는 바꿀 속성을 콕 집어 적는 게 좋아요. 우리 프로필 편집 버튼은 배경색만 변하니까, 배경색만 집었어요.
.btn-edit {
transition: background-color 0.2s ease;
}
.btn-edit:hover {
background-color: var(--color-border);
}
여러 속성을 한 번에 부드럽게 하고 싶으면, 쉼표로 나열하면 돼요. 예를 들면 transition: color 0.2s ease, background-color 0.2s ease;처럼요. all로 뭉뚱그리지 말고, 필요한 것만 골라 적는 습관을 들이세요.
Step 3: "transform — 움직이고, 돌리고, 키우고"
지금까지는 색만 바꿨어요. 이번엔 요소를 움직이고, 돌리고, 키워볼 거예요. 그걸 담당하는 게 **transform(변형)**이에요. 가장 많이 쓰는 세 가지부터요.
translate(x, y) 요소를 옆/위아래로 이동 ┌──┐ ┌──┐
└──┘ → └──┘ (오른쪽으로)
rotate(각도) 요소를 회전 ┌──┐ ◇
└──┘ → (15도 기울임)
scale(배율) 요소를 확대/축소 ┌──┐ ┌────┐
└──┘ → │ │ (1.15배)
└────┘
translate(x, y)— 가로 x, 세로 y만큼 이동.translateY(-4px)는 위로 4px(음수가 위쪽),translateX(10px)는 오른쪽으로 10px.rotate(각도)— 시계 방향으로 회전.rotate(15deg)는 15도.deg는 degree(도).scale(배율)— 크기 배율.scale(1.15)는 1.15배 크게,scale(0.9)는 0.9배(작게).
우리 게시물의 좋아요·댓글 같은 아이콘 버튼에, 마우스를 올리면 살짝 커지는 효과를 줘볼게요. transition과 transform을 함께 쓰는 거예요.
/* instagram-clone-frontend/css/components.css */
.icon-btn .ico {
transition: transform 0.15s ease;
}
.icon-btn:hover .ico {
transform: scale(1.15);
}
.icon-btn 버튼 안의 아이콘(.ico)에 평소엔 "변형이 생기면 0.15초 들여 부드럽게"라는 성격을 주고, 버튼에 마우스를 올리면(hover) 아이콘이 1.15배로 살짝 커지게 했어요. 마우스를 올리면 아이콘이 스르륵 부풀고, 떼면 다시 줄어들죠. 인스타그램의 아이콘들이 딱 이렇게 반응해요.
transform이 특별한 이유 — 주변을 안 밀어낸다
여기서 transform의 진짜 강점을 알아야 해요. 만약 아이콘을 키우려고 width를 키웠다면 어떻게 될까요? 아이콘이 커지면서 옆에 있는 다른 아이콘들을 밀어내요. 레이아웃이 출렁이죠.
width 로 키우면 (주변을 밀어냄 — 출렁임)
♡ 💬 ↗ → ♡ 💬 ↗ (♡가 커지며 옆을 밀어냄)
transform: scale 로 키우면 (자기 자리에서만 커짐 — 안정적)
♡ 💬 ↗ → ♡ 💬 ↗ (♡만 제자리에서 부풀고 옆은 그대로)
하지만 transform: scale은 달라요. 요소가 자기 자리를 차지한 채로, 그 위에서만 커지거나 움직여요. 주변 요소를 전혀 밀어내지 않죠. 그래서 레이아웃이 출렁이지 않아요.
이게 애니메이션에서 transform을 사랑하는 이유예요. 게다가 transform은 브라우저가 아주 효율적으로(그래픽 카드의 도움을 받아) 그릴 수 있어서 부드럽고 가벼워요. 그래서 "움직임은 가능하면 transform으로"가 실무의 기본 원칙이에요. 이 원칙은 Step 8에서 다시 정리할게요.
💡 직접 바꿔보세요.
scale(1.15)를scale(2)로 바꾸면 아이콘이 두 배로,rotate(15deg)를 추가하면 비스듬히,translateY(-4px)로 바꾸면 위로 살짝 떠올라요. 숫자 하나로 느낌이 확확 달라지는 걸 눈으로 확인해보세요.
Step 4: "눌리고, 빛나고 — hover / focus / active 삼총사"
애니메이션은 결국 "사용자의 행동에 화면이 반응하는 것"이에요. 이걸 인터랙션 디자인(interaction design)이라고 해요. 사용자가 할 수 있는 행동은 크게 세 가지고, CSS에는 거기 딱 맞는 가상 클래스(:로 시작하는 상태 선택자)가 있어요.
:hover 마우스를 올림 → "여기 누를 수 있어요" (살짝 커지거나 밝아짐)
:active 누르는 중 → "지금 눌렀어요" (살짝 눌리는 느낌)
:focus-visible 키보드로 이동해 옴 → "지금 여기 선택됨" (또렷한 테두리)
:hover는 Step 1~3에서 써봤죠. 이번엔 :active와 :focus-visible을 더해요.
:active — 누르는 순간 살짝 눌리게
버튼을 누르는 그 순간, 살짝 작아지면 "딸깍 눌렸다"는 촉감이 생겨요. 실제 버튼을 누르면 안으로 들어가는 것처럼요. :active는 "마우스로 누르고 있는 동안"을 뜻해요.
/* instagram-clone-frontend/css/components.css */
.icon-btn:active .ico {
transform: scale(0.9);
}
이제 아이콘을 클릭하면, 누르는 순간 0.9배로 살짝 쪼그라들었다가, 떼면 (Step 3의 hover 때문에) 다시 커져요. 올렸을 때 1.15배(:hover) → 눌렀을 때 0.9배(:active) → 떼면 다시 1.15배. 이 작은 반응 하나가 "버튼이 살아있다"는 느낌을 줘요.
:focus-visible — 키보드 사용자를 위한 또렷한 테두리
마우스 없이 키보드의 Tab 키만으로 사이트를 쓰는 사람도 있어요. 시각장애가 있거나, 손이 불편하거나, 그냥 키보드가 빠른 사람이요. 이들에게는 "지금 내가 어느 버튼에 있는지"가 또렷하게 보여야 해요. 그게 포커스 링(focus ring)이에요.
.icon-btn:focus-visible,
.suggest-follow:focus-visible,
.btn-login:focus-visible,
.btn-edit:focus-visible {
outline: 2px solid var(--ig-blue);
outline-offset: 2px;
}
:focus-visible은 "키보드로 이 요소에 도착했을 때만" 켜져요. 여기에 outline(요소 바깥 테두리)으로 파란 링을 둘렀어요. outline-offset은 그 링을 요소에서 2px 띄워서 더 또렷하게 보이게 하고요.
🙋 학생 질문 — "튜터님, :focus랑 :focus-visible은 뭐가 다른가요?"
좋은 질문이에요. :focus는 "이 요소가 선택되면 무조건" 켜져요. 마우스로 클릭했을 때도요. 그런데 문제는, 마우스로 버튼을 클릭했을 뿐인데도 파란 테두리가 떡 하니 남아서 "이거 왜 테두리가 생겼지?" 하고 어색해 보일 때가 많았어요.
그래서 나온 게 :focus-visible이에요. 브라우저가 똑똑하게 판단해서, 키보드로 이동해 왔을 때만 테두리를 보여주고, 마우스 클릭으로는 테두리를 안 보여줘요. 키보드 사용자에겐 꼭 필요한 안내를, 마우스 사용자에겐 깔끔한 화면을 주는 거죠. 그래서 요즘은 :focus 대신 :focus-visible을 기본으로 써요.
한 가지 주의! 포커스 링이 보기 싫다고 outline: none으로 아예 지워버리는 사람들이 있는데, 절대 하면 안 돼요. 키보드 사용자가 "내가 지금 어디 있는지" 완전히 길을 잃게 되거든요. 지우는 게 아니라, 우리처럼 예쁘게 디자인해주는 게 맞아요.
이렇게 :hover(올림)·:active(누름)·:focus-visible(키보드)을 갖추면, 마우스를 쓰든 키보드를 쓰든 모든 사용자가 "내 행동에 화면이 반응한다"는 걸 느껴요. 이게 잘 만든 인터랙션이에요.
Step 5: "좋아요 하트, 통! — @keyframes" (오늘의 산출물 ①)
드디어 오늘의 주인공, 좋아요 하트예요. 지금까지 배운 transition은 "A에서 B로", 두 지점 사이만 부드럽게 해줘요. 그런데 하트가 통! 튀는 동작을 떠올려보세요. 원래 크기 → 확 커졌다가 → 살짝 작아졌다가 → 제자리. 두 지점이 아니라 여러 지점을 거치죠.
이렇게 여러 구간을 거치는 애니메이션은 transition으로는 못 만들어요. 이때 쓰는 게 **@keyframes**예요. "이 시점엔 이 모습, 저 시점엔 저 모습"처럼 구간별 모습을 직접 정하는 거예요.
/* instagram-clone-frontend/css/components.css */
@keyframes heart-pop {
0% {
transform: scale(1);
}
30% {
transform: scale(1.4);
}
60% {
transform: scale(0.85);
}
100% {
transform: scale(1);
}
}
@keyframes heart-pop은 "heart-pop이라는 이름의 애니메이션 대본"이에요. 안에 적은 0%, 30%, 60%, 100%는 전체 시간 중 몇 퍼센트 지점인지를 뜻해요. 영화 콘티처럼요.
시간 → 0% 30% 60% 100%
│ │ │ │
크기: scale(1) scale(1.4) scale(0.85) scale(1)
● ◯ · ●
원래크기 확 커짐 살짝 작게 제자리
(통!) (반동)
0%(시작): 원래 크기.30%: 1.4배로 확 커져요. "통!" 하고 튀는 순간.60%: 0.85배로 살짝 작아져요. 튀어올랐다 떨어지는 반동 느낌.100%(끝): 다시 원래 크기로.
이 대본을 실제로 하트에 재생시켜야겠죠. 좋아요 버튼을 누르면 빨간 하트로 켜진다는 건, 이미 우리가 D-1에서 만들어뒀어요. 좋아요를 누르면 버튼에 is-active라는 표시가 붙도록요. 그 표시가 붙는 순간, 이 애니메이션을 한 번 재생하게 하면 돼요.
.icon-btn-like.is-active .ico {
animation: heart-pop 0.4s ease;
}
animation: heart-pop 0.4s ease를 풀어보면 "heart-pop 대본을, 0.4초 동안, ease 곡선으로 한 번 재생하라"예요. transition과 닮았지만, 첫 자리에 대본 이름(heart-pop)이 온다는 게 달라요.
여기서 멋진 점! 우리는 JavaScript를 한 줄도 건드리지 않았어요. 좋아요를 눌렀을 때 is-active를 붙이는 일은 이미 만들어둔 코드가 하고 있고, 우리는 그저 "is-active가 붙으면 이 애니메이션을 재생해"라는 CSS 규칙만 얹은 거예요. 동작(JS)과 표현(CSS)이 깔끔하게 나뉘어 있어서, CSS만으로 움직임을 입힐 수 있는 거죠.
이제 좋아요를 눌러보세요. 하트가 빨개지면서 통! 하고 튀어올라요. 밋밋하던 좋아요가 진짜 인스타그램처럼 살아났죠.
이미 있던 애니메이션 — 로딩 스피너
사실 우리는 이미 @keyframes를 한 번 쓴 적이 있어요. D-4에서 무한 스크롤을 만들 때, 빙글빙글 도는 로딩 스피너요. 그때 코드를 다시 보면 이래요.
.spinner {
/* ... 동그란 테두리 모양 ... */
animation: spin 0.7s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes spin은 구간이 딱 하나(to)예요. to는 100%와 같은 말이고, from은 0%와 같아요. "끝(to)에 360도 회전"만 정하면, 시작(0도)부터 끝(360도)까지 한 바퀴 도는 거죠. 그리고 animation의 마지막에 붙은 infinite는 "무한 반복"이에요. 그래서 데이터를 불러오는 동안 계속 빙글빙글 도는 거예요. linear를 쓴 것도 보이죠? 스피너는 일정한 속도로 돌아야 자연스러우니까요.
이렇게 @keyframes는 한 번 재생(하트)에도, 무한 반복(스피너)에도 쓸 수 있어요.
Step 6: "모달이 스르륵 — @starting-style" (오늘의 산출물 ②)
이번엔 더보기 메뉴와 공유 창이에요. 게시물의 더보기(⋯) 버튼을 누르면 메뉴가 뜨고, 공유 버튼을 누르면 공유 창이 떠요. 이 버튼들과 창(더보기 메뉴는 팝오버, 공유 창은 다이얼로그)은 A-4에서 이미 만들어뒀어요. 그런데 지금은 누르면 팍 하고 갑자기 나타나죠. 이걸 스르륵 떠오르게 만들 거예요.
그런데 여기서 Step 2의 그 문제가 발목을 잡아요. 기억하시죠? 모달은 평소엔 display: none(안 보임)이었다가 열릴 때 보이게 되는데, display는 transition이 안 된다고 했잖아요. 보임/안 보임 사이엔 중간이 없으니까요. 그래서 옛날엔 모달 등장 애니메이션을 만들기가 까다로웠어요(JavaScript의 도움을 받아야 했죠).
요즘 CSS는 이걸 깔끔하게 풀었어요. 핵심 도구가 **@starting-style**이에요. "이 요소가 화면에 나타나기 직전의 출발 모습"을 따로 정해주는 거예요.
생각해보면 당연해요. 애니메이션은 "출발 → 도착"이 있어야 하잖아요. 그런데 모달은 방금 전까지 display: none이라 화면에 아예 없었어요. 출발점이 없으니 애니메이션할 게 없었던 거죠. @starting-style은 "없다가 생길 때, 이 모습에서 출발해라"라고 출발점을 만들어줘요.
먼저 공유 창(다이얼로그)부터 볼게요. 우리는 이미 이 창의 생김새(둥근 카드, 그림자)는 다듬어뒀고, 여기에 등장 애니메이션을 입혀요.
/* instagram-clone-frontend/css/components.css */
#shareDialog {
opacity: 0;
transform: translateY(8px) scale(0.97);
transition:
opacity 0.2s ease,
transform 0.2s ease,
overlay 0.2s ease allow-discrete,
display 0.2s ease allow-discrete;
}
#shareDialog[open] {
opacity: 1;
transform: translateY(0) scale(1);
}
차근차근 볼게요.
#shareDialog(평소·닫힘): 투명하고(opacity: 0), 아래로 8px 내려가 살짝 작은(translateY(8px) scale(0.97)) 상태.#shareDialog[open](열림): 다이얼로그가 열리면open표시가 붙어요. 이때는 또렷하게(opacity: 1) 제자리·제크기(translateY(0) scale(1))로.
즉 "투명하고 살짝 아래 작게" → "또렷하고 제자리 제크기"로 변하면서 떠오르는 거예요. transition에 opacity와 transform을 적어 부드럽게 했고요.
그런데 transition 줄에 처음 보는 게 두 개 있죠. overlay와 display, 그리고 allow-discrete예요.
display ... allow-discrete— "display처럼 원래는 부드럽게 못 바꾸는 속성도, 애니메이션이 끝날 때까지 전환을 기다려달라"는 뜻이에요. 이게 있어야 모달이 사라질 때display: none이 애니메이션이 끝날 때까지 미뤄져서, 사라지는 애니메이션도 보여요.overlay— 다이얼로그가 화면 맨 위 레이어에 떠 있는 상태를 부드럽게 풀어주는 속성이에요. 역시allow-discrete와 함께 써요.
이 두 줄 덕분에 닫힐 때도 스르륵 사라져요. 자, 이제 마지막 한 조각, 출발점이에요.
@starting-style {
#shareDialog[open] {
opacity: 0;
transform: translateY(8px) scale(0.97);
}
}
@starting-style 안에 #shareDialog[open]을 또 적었죠. 이건 "다이얼로그가 열리는(open) 바로 그 순간의 출발 모습은 투명하고 아래 작게"라는 뜻이에요. 이게 없으면 브라우저는 출발점을 몰라서 등장 애니메이션을 그냥 건너뛰어요. 이 출발점이 있어야 "투명·아래·작게(출발)" → "또렷·제자리·제크기(도착)"로 부드럽게 떠오르는 거예요.
여기에 뒤 배경(backdrop)도 함께 어두워지게 하면 완성이에요. 다이얼로그 뒤에 깔리는 검은 막을 ::backdrop이라는 가상 요소로 다룰 수 있어요.
#shareDialog::backdrop {
background-color: rgb(0 0 0 / 0%);
transition:
background-color 0.2s ease,
overlay 0.2s ease allow-discrete,
display 0.2s ease allow-discrete;
}
#shareDialog[open]::backdrop {
background-color: rgb(0 0 0 / 50%);
}
@starting-style {
#shareDialog[open]::backdrop {
background-color: rgb(0 0 0 / 0%);
}
}
rgb(0 0 0 / 0%)는 완전 투명한 검정, rgb(0 0 0 / 50%)는 반투명한 검정이에요(/ 뒤가 불투명도). 닫혔을 땐 투명 → 열리면 반투명 검정으로 깔리면서, 뒤 화면이 서서히 어두워져요. 모달에 집중되는 그 느낌이죠.
더보기 메뉴(팝오버)도 완전히 같은 결로 처리했어요. 다이얼로그의 [open] 자리에 팝오버는 :popover-open을 쓴다는 것만 달라요.
#postMenu {
opacity: 0;
transform: translateY(6px);
transition:
opacity 0.15s ease,
transform 0.15s ease,
overlay 0.15s ease allow-discrete,
display 0.15s ease allow-discrete;
}
#postMenu:popover-open {
opacity: 1;
transform: translateY(0);
}
@starting-style {
#postMenu:popover-open {
opacity: 0;
transform: translateY(6px);
}
}
이제 더보기 버튼을 누르면 메뉴가 아래에서 스르륵 떠오르고, 공유 버튼을 누르면 창이 부드럽게 등장하면서 뒤가 어두워져요. 닫을 때도 똑같이 스르륵 사라지고요.
⚠️ 호환성 메모:
@starting-style과transition-behavior: allow-discrete는 2024년부터 모든 주요 브라우저(Chrome·Edge·Firefox·Safari)에서 동작하는 표준이에요. 만약 아주 오래된 브라우저에서 본다면, 애니메이션 없이 그냥 즉시 열리고 닫힐 뿐 기능은 멀쩡해요. 이렇게 "지원하면 더 예쁘게, 안 하면 평범하게라도 동작하게" 만드는 걸 점진적 향상(progressive enhancement)이라고 해요.
Step 7: "페이지가 끊기지 않는다 — View Transitions API"
지금까지는 한 페이지 안에서의 움직임이었어요. 이번엔 페이지와 페이지 사이예요.
평소에 링크를 클릭하면 어떻게 되죠? 현재 페이지가 휙 사라지고, 흰 화면이 잠깐 깜빡, 그리고 새 페이지가 뜨죠. 이 "흰 깜빡임"이 웹을 앱보다 투박하게 느끼게 하는 주범이에요. 진짜 앱은 화면이 부드럽게 밀려나거나 겹치면서 넘어가잖아요.
이걸 해결하는 게 View Transitions API예요. 이름은 거창하지만, 우리 같은 다중 페이지 사이트(여러 HTML 파일로 된)에서는 놀랍게도 CSS 딱 한 덩어리로 켤 수 있어요.
/* instagram-clone-frontend/css/components.css */
@view-transition {
navigation: auto;
}
이게 끝이에요. @view-transition { navigation: auto }는 "같은 사이트 안에서 페이지를 이동할 때, 자동으로 부드러운 전환을 켜라"는 뜻이에요. 우리 components.css는 모든 페이지(로그인·피드·프로필)가 함께 쓰니까, 이 한 줄로 페이지 사이 이동이 전부 부드러운 크로스페이드(이전 화면이 사라지며 새 화면이 떠오르는)로 바뀌어요. 피드에서 프로필로, 프로필에서 다시 피드로 넘어가 보면 흰 깜빡임 없이 스르륵 넘어가죠.
여기서 한 걸음 더. 페이지가 바뀌어도 "그대로 있어야 할 것"이 있어요. 예를 들면 위쪽의 'Instagram' 로고요. 피드에도 프로필에도 똑같이 있죠. 이 로고가 사라졌다 다시 나타나는 게 아니라, 제자리에서 자연스럽게 이어지게 할 수 있어요.
.logo {
view-transition-name: site-logo;
}
view-transition-name은 그 요소에 고유한 이름표를 붙이는 거예요. 두 페이지에 똑같이 site-logo라는 이름표가 붙은 요소가 있으면, 브라우저는 "아, 이건 같은 거구나" 하고 페이지가 바뀌는 동안 그 요소를 하나로 이어서 움직여요. 로고가 깜빡이지 않고 그 자리에 머무는 거죠. 이런 걸 공유 요소 전환이라고 해요.
⚠️ 호환성 메모: 페이지와 페이지 사이(다중 페이지) View Transitions는 2026년 6월 현재 Chrome·Edge 같은 크로미움 계열 브라우저에서 동작해요. Firefox와 Safari는 아직 지원 준비 중이에요(한 페이지 안에서의 전환은 이미 모든 브라우저가 지원해요). 다행히 지원하지 않는 브라우저에서는 그냥 예전처럼 평범하게 페이지가 넘어갈 뿐, 깨지거나 멈추지 않아요. Step 6에서 말한 점진적 향상이죠. "되는 곳에선 더 좋게, 안 되는 곳에서도 멀쩡하게." 그래서 이 한 줄은 지금 넣어도 전혀 위험하지 않아요.
딱 두 규칙(전환 켜기 + 로고 이름표)으로 우리 인스타그램이 페이지를 넘나들 때 앱처럼 매끄러워졌어요. 예전엔 JavaScript 없이는 꿈도 못 꾸던 게, 이제 CSS 몇 줄이 된 거예요.
Step 8: "멀미 없는 모션 — prefers-reduced-motion과 마무리 원칙"
오늘 우리는 화면을 많이 움직였어요. 그런데 여기서 꼭 짚어야 할 게 있어요. 모든 사람이 움직임을 반길까요?
화면의 큰 움직임에 어지럼증이나 멀미, 심하면 메스꺼움을 느끼는 분들이 있어요. 전정 기능(균형 감각)이 예민한 분들이요. 이런 분들을 위해 운영체제(Windows·맥·폰)에는 "동작 줄이기(Reduce Motion)"라는 설정이 있어요. 이걸 켜면 "나는 애니메이션을 줄여주세요"라고 신호를 보내는 거예요.
CSS는 그 신호를 들을 수 있어요. **prefers-reduced-motion**이에요.
/* instagram-clone-frontend/css/components.css */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
@media (prefers-reduced-motion: reduce)는 "사용자가 동작 줄이기를 켰다면"이라는 조건이에요. B-6에서 배운 미디어 쿼리가 화면 너비뿐 아니라 이런 사용자 설정도 조건으로 걸 수 있는 거죠. 그 안에서 모든 요소(*)의 애니메이션·전환 시간을 거의 0(0.01ms)으로 만들어, 움직임을 사실상 꺼요. 기능은 그대로 동작하되, 통 튀고 스르륵 떠오르는 움직임만 사라지는 거예요.
!important는 "이 규칙을 다른 규칙보다 무조건 우선하라"는 강한 명령이에요. 평소엔 남용하면 안 되지만, 이런 접근성 안전장치에서는 "사용자가 줄여달라면 무조건 줄인다"가 맞으니 예외적으로 써요.
이게 왜 중요하냐면, 애니메이션은 양념이거든요. 적당히 치면 음식이 살아나지만, 들이부으면 못 먹어요. 그래서 마지막으로 오늘의 원칙 두 가지를 정리할게요.
원칙 ① 움직임은 transform과 opacity 위주로
Step 3에서 말했죠. transform(이동·회전·크기)과 opacity(투명도)는 브라우저가 아주 가볍고 부드럽게 그려요. 반대로 width·height·top·left 같은 레이아웃 속성을 애니메이션하면, 브라우저가 매 순간 화면 배치를 다시 계산하느라 버벅여요. 그래서 "움직임은 가능하면 transform과 opacity로"가 철칙이에요. 오늘 우리가 하트도, 모달도, 아이콘도 전부 transform과 opacity로만 움직인 이유예요.
원칙 ② 적을수록 좋다
좋은 애니메이션은 "있는 줄도 모르게 자연스러운" 거예요. 사용자가 "오 애니메이션이다!" 하고 의식하는 순간, 그건 과한 거예요. 0.15~0.3초의 짧고 은은한 움직임이, 화려하게 1초씩 빙글빙글 도는 것보다 훨씬 세련됐어요. 인스타그램이 딱 그래요. 움직임은 분명 있는데, 거슬리지 않죠.
마무리
오늘은 멈춰 있던 인스타그램에 시간을 흘려, 진짜 앱처럼 살아 움직이게 만들었어요. 정리하면 이래요.
transition— 상태가 바뀔 때(hover 등) 시간을 들여 부드럽게. 평소 상태에 적고, 속성·시간·곡선 세 가지를 정해요.- 보간과 timing-function — 색·투명도·크기처럼 중간값이 있는 것만 부드러워지고(
display는 안 됨),ease·linear·cubic-bezier로 속도 곡선을 정해요. transform— 이동(translate)·회전(rotate)·크기(scale). 주변을 안 밀어내고 가벼워서 애니메이션의 일등 도구.:hover/:active/:focus-visible— 올림·누름·키보드 포커스. 모든 사용자에게 반응을 주는 인터랙션.@keyframes— 여러 구간을 거치는 애니메이션. 좋아요 하트가 통! 튀는 효과를 JavaScript 없이 완성.@starting-style— 없다가 생기는 모달·메뉴의 출발점을 만들어, 스르륵 등장·퇴장.- View Transitions API —
@view-transition한 줄로 페이지 사이를 끊김 없이. 공유 요소는 이름표로 이어요. prefers-reduced-motion— 움직임에 민감한 사용자를 배려하는 접근성 가드.
여기까지 오면서 느꼈겠지만, 오늘 한 일 전부 CSS만으로 했어요. JavaScript는 한 줄도 안 건드렸죠. 좋아요를 켜는 동작은 이미 만들어둔 코드가 하고, 우리는 거기에 "표현"만 입혔어요. 동작과 표현이 깔끔하게 나뉘어 있다는 게 이래서 좋은 거예요.
다음 시간(B-8)에는 CSS의 마지막 조각, CSS 변수(Custom Properties)를 배워요. 지금 우리 코드 곳곳에 흩어진 --ig-blue, --color-text 같은 값들을 한곳에서 관리하고, 그걸 활용해 다크 모드(어두운 테마)까지 만들 거예요. 오늘 색을 부드럽게 전환했던 것처럼, 다음 시간엔 그 색들 자체를 통째로 갈아끼우는 마법을 보게 될 거예요.
과제
오늘 배운 애니메이션을 직접 손에 익혀볼 차례예요. 모든 과제는 지금까지 배운 HTML과 CSS만으로 충분히 해낼 수 있어요. Live Server로 페이지를 열어두고, 값을 바꿔가며 눈으로 확인하면서 진행하세요.
[구현] 팔로우 버튼을 더 살아있게
추천 사이드바의 팔로우 버튼(.suggest-follow)은 지금 색만 부드럽게 변해요. 여기에 Step 3·4에서 배운 움직임을 더해 더 생생하게 만들어보세요.
- 마우스를 올리면(
:hover) 색이 변하면서 살짝 커지게(transform: scale(1.05)정도) 해보세요. - 누르는 순간(
:active)엔 살짝 작아지게(scale(0.95)) 해서 눌리는 느낌을 주세요. transition에color와transform을 둘 다 넣어야 두 변화가 모두 부드러워져요. 쉼표로 나열하는 걸 잊지 마세요.- 키보드 Tab으로 버튼에 도착했을 때 또렷한 테두리가 보이도록
:focus-visible도 더해보세요.
[구현] 스토리 링을 천천히 돌리기
피드 위쪽 스토리의 무지개 테두리(.story-ring)에 마우스를 올리면, 그 무지개가 천천히 한 바퀴 도는 효과를 만들어보세요. Step 5의 @keyframes를 응용하는 과제예요.
@keyframes로rotate(0deg)에서rotate(360deg)까지 도는 대본을 하나 만드세요(스피너의spin을 참고해도 좋아요)..story-ring:hover에 그 애니메이션을 걸되, 마우스를 올리고 있는 동안 계속 돌도록infinite(무한 반복)와linear(일정한 속도)를 써보세요.- 회전 시간을
3s,8s등으로 바꿔보며 "너무 빠르면 어지럽고, 너무 느리면 안 도는 것 같은" 적당한 속도를 찾아보세요.
[탐구] cubic-bezier와 동작 줄이기 체험
오늘 살짝만 언급한 두 가지를 직접 체험하고 정리해보세요.
- Step 5의 하트 애니메이션
animation에서ease를cubic-bezier(0.68, -0.55, 0.27, 1.55)로 바꿔보세요. 음수가 섞인 이 곡선이 하트를 어떻게 튀게 하는지(통 튀어올라 넘쳤다 돌아오는 느낌) 관찰하고 한 문장으로 적어보세요. - 운영체제 설정에서 "동작 줄이기(Reduce Motion)"를 켜보세요(맥: 손쉬운 사용 → 디스플레이, Windows: 접근성 → 시각 효과). 그 상태로 좋아요를 눌러보면 하트 애니메이션이 어떻게 달라지나요? 우리가 Step 8에서 단
prefers-reduced-motion규칙이 실제로 동작하는지 확인하고, 이 배려가 왜 필요한지 한두 문장으로 정리해보세요.
생각해볼 주제
정답을 적는 문제가 아니에요. 오늘 배운 도구의 "왜"를 곱씹어보는 질문들이에요. 스스로 답을 만들어본 뒤, 예시답안과 비교해보세요.
1. transition과 @keyframes, 언제 무엇을 쓸까?
둘 다 화면을 부드럽게 움직인다는 점은 같아요. 그런데 좋아요 하트의 "통!"은 @keyframes로 만들었고, 버튼 색 변화는 transition으로 만들었죠. 만약 하트 효과를 transition으로 만들려고 하면 왜 어려울까요? 반대로 단순한 색 변화에 @keyframes를 쓰면 왜 과할까요? "두 지점 사이"와 "여러 구간"이라는 차이, 그리고 "상태가 바뀔 때 자동으로"와 "정해진 대본을 재생"이라는 차이에서 출발해 생각해보세요.
2. 애니메이션은 왜 transform과 opacity만 권할까?
요소를 키우려면 width를 키워도 되고 transform: scale을 써도 돼요. 위로 올리려면 top을 바꿔도 되고 transform: translateY를 써도 되고요. 결과는 비슷해 보이는데, 왜 실무에서는 한사코 transform과 opacity만 권할까요? "브라우저가 화면을 그리는 과정"과 "주변 요소를 밀어내는가"라는 두 관점에서 생각해보세요. 60장의 그림을 1초에 그려야 부드럽게 보인다는 점도 힌트예요.
3. 움직임은 많을수록 좋을까?
오늘 우리는 화면 곳곳에 움직임을 넣었어요. 그런데 만약 모든 버튼이 빙글빙글 돌고, 모든 글자가 번쩍이고, 페이지마다 화려한 전환이 1초씩 걸린다면 어떨까요? prefers-reduced-motion이라는 설정이 운영체제에 아예 존재한다는 사실이 무엇을 말해줄까요? "양념으로서의 애니메이션", 그리고 "애니메이션이 정보를 전달하는가, 아니면 그냥 화려한가"라는 기준에서, 좋은 움직임과 과한 움직임의 경계를 생각해보세요.
✅ 예시 답안정답 보기
과제와 생각해볼 주제의 예시답안이에요. 정답이 하나만 있는 건 아니에요. 시간 값(0.2s 등)이나 배율(1.05 등)은 취향대로 골라도 좋아요. 중요한 건
transition은 평소 상태에 적었는가, 움직임은transform으로 했는가, 그리고 눈으로 직접 확인했는가 예요.
🎯 [과제 1 예시답안] 팔로우 버튼을 더 살아있게
핵심 접근
지금 팔로우 버튼은 색만 부드럽게 변해요. 여기에 Step 3·4에서 배운 transform과 상태 선택자를 더하는 과제예요. 포인트는 두 가지예요. 첫째, transition에 color와 transform을 둘 다 나열해야 두 변화가 모두 부드러워져요. 둘째, transition은 평소 상태(.suggest-follow)에 한 번만 적고, :hover·:active엔 "도착 모습"만 적는 거예요.
예시 구현
/* instagram-clone-frontend/css/components.css */
.suggest-follow {
transition: color 0.2s ease, transform 0.2s ease;
}
.suggest-follow:hover {
color: var(--ig-blue-strong);
transform: scale(1.05);
}
.suggest-follow:active {
transform: scale(0.95);
}
.suggest-follow:focus-visible {
outline: 2px solid var(--ig-blue);
outline-offset: 2px;
}
평소 상태에 transition: color ..., transform ...으로 두 속성을 묶어 적었어요. 마우스를 올리면 진한 파랑으로 물들면서 1.05배로 살짝 부풀고, 누르는 순간 0.95배로 눌렸다가, 떼면 다시 hover 크기로 돌아와요. 키보드 Tab으로 도착하면 파란 포커스 링도 또렷이 보이고요. 색·확대·눌림·포커스가 한 버튼에 모두 모인 거죠.
채점 포인트
| 항목 | 확인 내용 |
|---|---|
| transition 위치 | transition을 평소 상태(.suggest-follow)에 적었는가 (:hover가 아니라) |
| 다중 속성 | color와 transform을 쉼표로 둘 다 나열했는가 |
| hover 확대 | :hover에 transform: scale(1.05) 같은 확대를 줬는가 |
| active 눌림 | :active에 scale(0.95) 같은 축소로 눌리는 느낌을 줬는가 |
| 포커스 | :focus-visible로 키보드 사용자를 위한 테두리를 남겼는가 |
흔한 실수
transition을:hover에 적음 — 그러면 올라갈 때만 부드럽고 떼는 순간엔 툭 끊겨요. transition은 그 요소의 "성격"이라 평소 상태에 적어야 양방향 모두 부드러워요.transition: color만 두고transform을 안 넣음 — 그러면 색은 부드러운데 크기 변화는 툭 점프해요. 바꿀 속성을 전부 나열해야 해요. (또는all을 쓸 수도 있지만, Step 2에서 봤듯 콕 집어 적는 게 권장이에요.)outline: none으로 포커스 링을 지움 — 깔끔해 보여도 키보드 사용자가 길을 잃어요. 지우지 말고 예쁘게 디자인하세요.
🎯 [과제 2 예시답안] 스토리 링을 천천히 돌리기
핵심 접근
스토리의 무지개 테두리(.story-ring)에 마우스를 올리면 천천히 한 바퀴 돌게 하는 과제예요. "여러 구간"이 아니라 "0도 → 360도 한 바퀴"라 @keyframes는 끝(to)만 정하면 돼요. 로딩 스피너의 spin과 똑같은 구조예요. 핵심은 마우스를 올리고 있는 동안 계속 돌도록 infinite(무한 반복)와, 일정한 속도로 돌도록 linear를 쓰는 거예요.
예시 구현
/* instagram-clone-frontend/css/components.css */
@keyframes ring-spin {
to {
transform: rotate(360deg);
}
}
.story-ring:hover {
animation: ring-spin 6s linear infinite;
}
@keyframes ring-spin은 끝(to)에 rotate(360deg)만 정했어요. 시작(0도)은 안 적어도 현재 상태(0도)에서 출발하니까요. .story-ring:hover에 animation: ring-spin 6s linear infinite를 걸어, 마우스를 올리는 동안 6초에 한 바퀴씩 일정한 속도로 계속 돌게 했어요. 마우스를 떼면 hover가 풀려 멈추고요.
채점 포인트
| 항목 | 확인 내용 |
|---|---|
| @keyframes 회전 | to { transform: rotate(360deg) }로 한 바퀴 대본을 만들었는가 |
| hover에 연결 | .story-ring:hover에 animation을 걸었는가 |
| 무한 반복 | infinite로 누르는 동안 계속 돌게 했는가 |
| 일정한 속도 | linear로 균일하게 돌게 했는가 (회전엔 ease보다 자연스러움) |
| 속도 실험 | 시간 값(3s·8s 등)을 바꿔보며 적당한 속도를 찾았는가 |
흔한 실수
width나margin으로 회전을 흉내 — 회전은transform: rotate가 정답이에요. 다른 속성으론 돌릴 수 없어요.infinite를 안 씀 — 그러면 한 바퀴만 돌고 멈춰요. 마우스를 올리고 있는 내내 돌게 하려면infinite가 필요해요.linear대신ease— 회전에ease를 쓰면 한 바퀴마다 빨라졌다 느려졌다 출렁여서 어색해요. 균일한 회전엔linear가 어울려요.
🎯 [과제 3 예시답안] cubic-bezier와 동작 줄이기 체험
핵심 접근
코드를 새로 짜기보다, 이미 만든 하트 애니메이션의 값 하나를 바꿔 관찰하고, 운영체제 설정을 켜서 접근성 규칙이 진짜 동작하는지 확인하는 탐구예요. "왜"를 눈으로 확인하는 게 목적이에요.
예시 관찰 — cubic-bezier로 바꿨을 때
Step 5의 하트 애니메이션에서 ease를 cubic-bezier(0.68, -0.55, 0.27, 1.55)로 바꾸면 이렇게 돼요.
.icon-btn-like.is-active .ico {
animation: heart-pop 0.4s cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
관찰 예시: "
ease일 때는 하트가 얌전하게 커졌다 돌아왔는데, 이 cubic-bezier로 바꾸니 하트가 목표 크기를 넘어서까지 확 튀어올랐다가 다시 제자리로 당겨지는, 더 탄력 있는 '통!' 느낌이 났어요. 곡선 값에 음수(-0.55)와 1을 넘는 값(1.55)이 섞여 있어서, 시작점 아래로 살짝 눌렀다가 도착점을 넘겨 튕기는 '오버슈트'가 생긴 거예요."
cubic-bezier의 네 숫자를 일일이 외울 필요는 없어요. 음수나 1 초과 값을 넣으면 "튕기는" 느낌이 난다는 것만 감으로 알아두면 충분해요.
예시 관찰 — 동작 줄이기를 켰을 때
운영체제에서 "동작 줄이기(Reduce Motion)"를 켜고 좋아요를 눌러보면 이렇게 돼요.
관찰 예시: "동작 줄이기를 켜기 전엔 하트가 통! 하고 튀었는데, 켜고 나니 하트가 튀는 움직임 없이 곧바로 빨간색으로 켜졌어요. 좋아요 기능 자체(빨개지고 숫자가 오르는 것)는 그대로 동작하고, 통 튀는 '움직임'만 사라진 거예요. Step 8에서 단
prefers-reduced-motion규칙이 실제로 애니메이션 시간을 0에 가깝게 줄여준 거죠."
채점 포인트
| 항목 | 확인 내용 |
|---|---|
| cubic-bezier 관찰 | 음수·1 초과 값이 만드는 "튕김(오버슈트)"을 눈으로 확인하고 적었는가 |
| 설정 켜기 | 운영체제의 동작 줄이기를 실제로 켜봤는가 |
| 기능 vs 움직임 | 기능은 그대로 동작하고 "움직임만" 사라진 걸 구분해 적었는가 |
| 배려의 이유 | 이 배려가 왜 필요한지(멀미·어지럼 등) 한 문장으로 정리했는가 |
흔한 실수
prefers-reduced-motion을 "애니메이션을 완전히 없애는 것"으로 오해 — 기능까지 없애는 게 아니에요. 좋아요는 그대로 켜지고, 통 튀는 "움직임"만 줄어드는 거예요. 정보 전달은 유지하되 거슬리는 모션만 빼는 거죠.
💭 [생각해볼 주제 예시답안]
1. transition과 @keyframes, 언제 무엇을 쓸까?
둘 다 화면을 부드럽게 움직이지만, 모양과 계기가 달라요.
transition은 "두 지점 사이"예요. 평소 모습(A)과 어떤 상태의 모습(B), 딱 두 점을 부드럽게 이어요. 그리고 상태가 바뀔 때 자동으로 발동해요. 마우스를 올리면(hover), 클래스가 붙으면, 그 변화를 알아서 부드럽게 메워주죠. 그래서 버튼 색 변화처럼 "A에서 B로 한 번 넘어가는" 단순한 변화에 딱이에요.
@keyframes는 "여러 구간"이에요. 0% → 30% → 60% → 100%처럼 중간 지점들을 직접 정해 대본을 짜요. 좋아요 하트의 "원래 → 확 커짐 → 살짝 작아짐 → 제자리"는 두 점으로는 표현이 안 돼요. transition으로 만들려면 "커짐"과 "작아짐"을 두 단계로 쪼개 어렵게 이어붙여야 하는데, @keyframes는 그 구간들을 한 대본에 자연스럽게 담아요.
반대로 단순한 색 변화에 @keyframes를 쓰면, 대본을 따로 만들고 animation으로 재생까지 시켜야 해서 과해요. transition 한 줄이면 될 일을요. 그래서 기준은 이래요. "두 점 사이 + 상태 변화에 자동 반응"이면 transition, "여러 구간을 거치거나 + 스스로 재생/반복"이면 @keyframes. 로딩 스피너처럼 계속 도는 것도 @keyframes의 몫이죠(상태 변화가 아니라 계속 반복이니까요).
🎯 면접관을 홀리는 핵심 멘트
"
transition은 두 지점 사이를, 상태가 바뀔 때 자동으로 부드럽게 이어요. 버튼 hover 색 변화처럼 'A→B 한 번'에 어울리죠.@keyframes는 여러 구간을 직접 짠 대본을 재생·반복해요. 하트가 커졌다 작아졌다 제자리로 오는 다단계 동작이나, 무한히 도는 스피너처럼요. '두 점이냐 여러 구간이냐, 상태 반응이냐 대본 재생이냐'로 나누면 선택이 분명해집니다."
2. 애니메이션은 왜 transform과 opacity만 권할까?
같은 결과를 내는 방법이 여럿이어도, transform과 opacity를 권하는 데는 두 가지 이유가 있어요.
첫째, 브라우저가 화면을 그리는 과정 때문이에요. 브라우저는 화면을 그릴 때 대략 "어디에 얼마나 큰가 계산(레이아웃) → 색칠(페인트) → 합치기(합성)" 순서를 거쳐요. width·height·top·left 같은 속성을 바꾸면 맨 앞 단계인 레이아웃 계산부터 다시 해야 해요. 그런데 애니메이션은 부드럽게 보이려면 1초에 약 60장의 그림을 그려야 하거든요. 매 장마다 무거운 레이아웃 계산을 다시 하면 버벅여요. 반면 transform과 opacity는 맨 마지막 "합치기" 단계만 건드려서, 브라우저가 (그래픽 카드의 도움까지 받아) 아주 가볍게 그려요.
둘째, 주변을 밀어내는가 예요. width로 요소를 키우면 옆·아래 요소들을 실제로 밀어내서 레이아웃이 출렁여요. transform: scale은 자기 자리를 차지한 채 그 위에서만 커져서 주변을 건드리지 않죠. 출렁임 없이 깔끔한 이유예요.
정리하면, transform·opacity는 가볍고(60fps 유지) + 주변을 안 흔든다는 두 장점이 있어요. 그래서 "움직임은 가능하면 이 둘로"가 철칙이 된 거예요.
🎯 면접관을 홀리는 핵심 멘트
"브라우저는 레이아웃 → 페인트 → 합성 순으로 화면을 그리는데,
width나top을 바꾸면 맨 앞 레이아웃부터 다시 계산해요. 애니메이션은 1초에 60장을 그려야 하니 매 장 무거운 재계산이 끼면 버벅이죠.transform·opacity는 마지막 합성 단계만 건드려 GPU 가속까지 받아 가볍고, 게다가 주변을 밀어내지 않아 출렁임도 없어요. 그래서 부드러운 애니메이션의 기본 도구입니다."
3. 움직임은 많을수록 좋을까?
아니에요. 애니메이션은 양념이에요. 적당히 치면 화면이 살아나지만, 들이부으면 못 봐줘요.
기준은 이 질문이에요. "이 움직임이 정보를 전달하는가, 아니면 그냥 화려한가?" 좋아요 하트가 통 튀는 건 "네 행동이 먹혔어!"라는 피드백을 줘요. 모달이 떠오르는 건 "새 창이 위에 떴어"라는 공간 감각을 줘요. 이건 정보를 전달하는 좋은 움직임이에요. 반대로 모든 글자가 번쩍이고 버튼마다 빙글빙글 돈다면, 전달하는 정보 없이 시선만 빼앗고 피로하게 만들죠.
그리고 운영체제에 prefers-reduced-motion이라는 설정이 아예 존재한다는 사실이 많은 걸 말해줘요. 어떤 사람에겐 화면의 큰 움직임이 어지럼증이나 멀미를 일으킨다는 거예요. 즉 과한 움직임은 단순히 "촌스럽다"를 넘어 누군가에겐 고통일 수 있어요. 그래서 좋은 움직임은 짧고(0.15~0.3초), 은은하고, 의미가 있어요. 사용자가 "오 애니메이션이다!" 하고 의식하는 순간 이미 과한 거예요. 있는 듯 없는 듯 자연스러운 게 가장 세련된 움직임이에요.
🎯 면접관을 홀리는 핵심 멘트
"애니메이션은 양념이라, 정보를 전달할 때만 써야 해요. 좋아요 하트의 튐은 '행동이 먹혔다'는 피드백을, 모달의 등장은 '위에 떴다'는 공간감을 주죠. 반면 의미 없이 화려하기만 한 움직임은 시선만 빼앗아요. 게다가
prefers-reduced-motion설정이 존재한다는 건, 과한 모션이 누군가에겐 어지럼·멀미가 된다는 뜻이에요. 그래서 좋은 모션은 짧고 은은하고 의미 있게, 그리고 줄여달라는 사용자에겐 반드시 줄여주는 것까지가 한 세트입니다."