본문으로 건너뛰기

© 2026 Molayo

CSS-T헤드라인2026. 06. 22. 21:51

반대 방향 스크롤을 위한 스크롤 기반 애니메이션 (Scroll-Driven Animations) 사용하기

요약

현대적인 CSS의 스크롤 기반 애니메이션(Scroll-Driven Animations) 기능을 활용하여, 사용자의 스크롤 방향과 반대로 움직이는 컬럼 애니메이션을 구현하는 방법을 설명합니다. 가상 요소와 마스킹 기법을 통해 아이템이 경계를 넘을 때 자연스럽게 사라지는 시각 효과를 만드는 과정을 다룹니다.

핵심 포인트

  • CSS 스크롤 기반 애니메이션을 이용한 역방향 스크롤 구현
  • 가상 요소(:before, :after)를 활용한 마스킹 효과 적용
  • 쌓임 맥락(Stacking Context)을 이용한 시각적 레이어 제어
  • 반응형 디자인을 고려한 미디어 쿼리 적용 방법

때로는 디자이너들의 엉뚱한 아이디어가 결국 당신의 마음에 들게 되는 경우가 있습니다. 저에게는 사용자가 페이지를 스크롤할 때 아이템들이 서로 반대 방향으로 움직이는 컬럼(column)을 구축해야 했던 이 개념이 바로 그런 경우였습니다.

CodePen Embed Fallback

참고: 이 데모는 동작 감소(reduced motion) 설정을 준수하므로, 효과를 보려면 모션(motion)을 활성화해야 합니다. 또한, 이 글을 쓰는 시점 기준으로 Chrome 및 Safari 지원을 기준으로 작성되었습니다.

현대적인 CSS 기능, 특히 스크롤 기반 애니메이션 (scroll-driven animations) 덕분에 생각만큼 어렵지 않습니다. 그뿐만 아니라 만드는 과정도 매우 즐겁습니다! 제가 어떻게 접근했는지 보여드릴 테니, 여러분은 어떻게 다르게 구현할지 공유해 주셔도 좋습니다.

HTML

HTML은 부모 요소(.opposing-columns), 자식 요소(.opposing-column), 그리고 자식의 자식 요소(.opposing-item)로 구성됩니다:

<div class="opposing-columns">
  <!-- Column 1 -->
  <div class="opposing-column">
...

마크업에는 이것만 있으면 됩니다. 나머지는 CSS가 처리할 것입니다!

부모 컨테이너 스타일링

먼저, 이 효과가 큰 화면에서만 적용되도록 설정하겠습니다. 이 효과를 구현하려면 추가적인 공간이 필요하기 때문에 작은 화면에서 이를 지원하는 것은 큰 의미가 없습니다.

/* 큰 화면에서만 적용 */
@media screen and (width >= 50rem) {
  .opposing-columns {
...

"마스킹 (masking)" 효과 설정하기

.opposing-column의 아이템들이 스크롤되어 지나갈 때 사라지는 듯한 착각을 주려면 부모 컨테이너에 몇 가지 작업을 더 해야 합니다. 바깥쪽 컬럼의 아이템들은 스크롤 시 위로 움직이고, 중앙 컬럼의 아이템들은 아래로 움직입니다. 이들이 부모의 경계를 넘을 때, 마치 서서히 사라지는(fade out) 것처럼 보이게 하고 싶습니다.

따라서 몇 가지 작업을 수행할 것입니다. 먼저, 문서 전체에 배경색 변수를 설정하겠습니다:

@media screen and (width >= 50rem) {
  :root {
    --opposing-bg: lightcyan;
...

둘째로, 부모 요소의 :before:after 가상 요소 (pseudo-elements)에 동일한 배경색을 적용하겠습니다.

@media screen and (width >= 50rem) {
  :root {
    --opposing-bg: lightcyan;
...

가상 요소들에 쌓임 맥락 (stacking context)을 설정하여, 부모 요소 및 그 자손 요소들보다 한 단계 위에 배치했다는 점에 주목하세요. 이는 각 열 (column)의 아이템들이 컨테이너 안으로 들어오거나 밖으로 나갈 때 마스킹 (masking) 처리를 하기 위한 핵심 요소입니다. 기술적으로 아이템들은 가상 마스크 아래로 미끄러져 들어가는 방식입니다.

이와 관련하여, 부모 요소와 세 개의 열 사이에 수직 공간을 추가하는 --opposing-mask라는 변수를 하나 더 만들어 보겠습니다.

@media screen and (width >= 50rem) {
  :root {
    --opposing-bg: lightcyan;
...

Highlighting the vertical space between the parent container and its child elements.

부모 요소의 가상 요소에도 동일한 작업을 수행하되, --opposing-maskblock-size에 3의 배수로 적용하겠습니다. 이렇게 하면 가상 요소와 부모 요소 사이에 추가적인 수직 공간이 생깁니다.

@media screen and (width >= 50rem) {
  :root {
    --opposing-bg: lightcyan;
...

Highlighting the vertical space between the parent container and its before pseudo element.

이 작업이 어디로 향하고 있는지 짐작이 가실 겁니다. 부모 컨테이너와 가상 요소 사이에 적절한 공간이 확보되었습니다. 우리는 열 아이템들이 부모 컨테이너 밖으로 스크롤되어 나갈 때 마치 서서히 사라지는 (fading out) 것처럼 보이게 만들고 싶습니다. 아이템의 불투명도 (opacity) 등을 직접 건드릴 필요는 없습니다. 대신 가상 요소에 배경 그라데이션 (background gradients)을 추가하면 됩니다.

:before 가상 요소는 컨테이너의 상단에 있으므로, 문서의 기본 배경색과 일치하는 단색에서 투명색으로 변하는 상단에서 하단 방향의 그라데이션을 적용하겠습니다. 그리고 :after 가상 요소는 부모 컨테이너의 하단에 위치하므로, 그라데이션을 반대로 적용하여 하단에서 상단 방향으로 투명색에서 문서 배경색으로 변하도록 하겠습니다.

@media screen and (width >= 50rem) {
  :root {
    /* 이전과 동일한 스타일 */
...

열 레이아웃 (The column layouts)

마법 같은 효과를 보기 전에, 각 열(column)에 아이템들을 배치해야 합니다. 각 열은 부모 요소인 플렉스 컨테이너(flex container) 내부의 플렉스 아이템(flex item)입니다. 우리는 아이템들이 줄어들거나(flex-shrink: 1) 늘어날 수 있게(flex-grow: 1) 하되, 크기는 특정 지점(flex-basis: 10rem)에서 제한되도록 설정할 것입니다.

이 모든 것을 [flex](https://css-tricks.com/almanac/properties/f/flex/) 단축 속성(shorthand property)으로 정의할 수 있습니다:

@media screen and (width >= 50rem) {
  /* 이전과 동일한 스타일 */

...

이제 이 열들을 그리드 컨테이너(grid container)로 만들어 gap 속성을 사용하여 아이템 사이에 간격을 삽입하고 싶습니다:

@media screen and (width >= 50rem) {
  /* 이전과 동일한 스타일 */

...

gap을 사용하기 위해 여기서 Flexbox를 사용할 수도 있었겠지만, 기본 레이아웃이 row로 설정되어 있어 이를 column으로 재정의해야 합니다. 이 상황에서는 Grid가 조금 더 간결합니다.

애니메이션!

이것이 여러분이 기대하던 것이죠? 우리는 스크롤 시 열 아이템들이 부모 컨테이너 안으로 들어오고 나갈 수 있도록 모든 설정을 마쳤습니다. 이제 그 스크롤 동작을 추가해야 합니다.

여기서 animation-timeline 속성이 매우 유용하게 쓰입니다. 보통 CSS 애니메이션은 스스로 실행됩니다. 페이지가 로드될 때(또는 설정한 특정 지연 시간 후에) 시작되어 설정한 지속 시간(duration)이 지나면 종료됩니다. animation-timeline을 사용하면 애니메이션이 스크롤 위치(scroll position)에 따라 실행되도록 명령할 수 있습니다... 그래서

부모 컨테이너 내부에 명확한 스크롤 가능한 영역(scrollable area)이 있도록 설정했기 때문에 우리는 view() 함수를 사용할 것입니다. 우리는 컬럼 아이템(column items)의 스크롤 위치가 아니라, 해당 영역에 진입하고 나가는 시점을 기준으로 애니메이션을 실행해야 합니다.

이 부분은 매우 흥미로운데, view()에게 요소가 스크롤 가능한 영역에 *진입(enters)*할 때 애니메이션을 정확히 어디서 시작할지, 그리고 동일한 영역을 벗어날(exits) 때 어디서 멈출지를 알려줄 수 있기 때문입니다. 다음과 같이 말이죠:

/* 공식 문법 */
animation-timeline:  view([ <axis> || <'view-timeline-inset'>]?);

먼저 축(axes)을 정의하는 것부터 시작하겠습니다:

@media screen and (width >= 50rem) {
  /* 이전과 동일한 스타일 */

...

이것은 우리가 원하는 것의 일부일 뿐이지만, 우리가 말하고자 하는 바는 애니메이션이 (1) 스크롤포트(scrollport)에 진입하는 바로 그 순간(entry)에 시작하고, (2) 영역을 완전히 벗어날 때(cover) 종료되기를 원한다는 것입니다. 인셋(insets)을 명시적으로 설정해야 하는데, 이는 요소가 진입하고 나가는 지점에 대한 애니메이션의 범위(range)를 결정하기 때문입니다. 우리는 전체 범위를 원하므로, entry0%에서 시작하고 종료는 아이템이 100%만큼 cover되었을 때입니다.

@media screen and (width >= 50rem) {
  /* 이전과 동일한 스타일 */

...

마지막으로, 애니메이션이 선형(linearly)으로 실행되도록 설정하겠습니다. 아이템이 스크롤될 때 속도가 느려지거나 빨라질 필요는 없습니다.

@media screen and (width >= 50rem) {
  /* 이전과 동일한 스타일 */

...

좋습니다. 하지만 아직 애니메이션을 생성하지는 않았습니다. 애니메이션이 실행될 때 무엇을 할지는 설정했지만, 실제 움직임을 정의해야 합니다.

저는 세 개의 별도 CSS 애니메이션을 설정하고 싶습니다:

  1. 첫 번째 컬럼에서 아이템을 위로 이동(translate)시키는 애니메이션 하나.
  2. 다른 컬럼의 아이템들을 위해 첫 번째 애니메이션을 역재생(reverse)하는 애니메이션 하나.

기술적으로는 두 개의 외부 컬럼 모두에 첫 번째 애니메이션을 설정할 수도 있지만, 저는 컬럼들이 엇갈려 보이도록(staggered) 첫 번째 애니메이션에서 약간 오프셋(offset)을 둔 세 번째 애니메이션을 만들고 싶습니다.

@keyframes scroll1 {
  from { transform: translateY(var(--opposing-mask)); }
  to { transform: translateY(calc(var(--opposing-mask) * -1)); }
} 
...

물론, 나중에 업데이트가 필요할 경우를 대비해 이 값들을 위한 변수들을 만들 수 있습니다:

@media screen and (width >= 50rem) {
  :root {
    --opposing-bg: lightcyan;
...

...그리고 각 컬럼(column)에 이를 적용합니다:

@media screen and (width >= 50rem) {
  /* 이전과 동일한 스타일 */

...

이왕 하는 김에, 사용자의 동작 감소(reduced motion) 설정을 존중하기 위해 애니메이션을 비활성화해야 합니다 (또한 마스크(mask)를 제거해야 합니다. 그렇지 않으면 이상하게 보일 수 있습니다):

@media (prefers-reduced-motion: reduce) {
  .opposing-column {
    animation: unset;
...

마무리하며

네, 그렇습니다. 스크롤 기반 애니메이션(scroll-driven animations)은 정말, 정말 멋집니다. 이 글을 쓰는 시점에도 여전히 Firefox의 지원을 기다리고 있지만, @supports로 감싸서 스크롤 어노테이션(scroll annotations)을 사용하는 기본 경험을 제공하고, 지원하지 않는 브라우저를 위해 일반 애니메이션 타임라인(animation timeline)에서 실행되는 것과 같은 폴백(fallback) 경험을 설정할 수 있습니다:

@supports (animation-timeline: view()) {
  /* ... */
}

물론 이것은 스크롤 기반 애니메이션이 할 수 있는 일의 아주 일부분일 뿐입니다. 여러분은 어떤 것들을 만들거나 실험해 보셨나요? 아니면 이 작업을 다른 방식으로 접근하시겠습니까? 알려주세요!

AI 자동 생성 콘텐츠

본 콘텐츠는 CSS-Tricks의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0