마키(marquee)는 텍스트가 제한된 영역 내에서 자동으로 가로로 스크롤되는 시각 효과로, 긴 문자열을 컴팩트한 컨테이너에 표시할 수 있습니다. 역사적으로 이 용어는 1990년대 웹에서 인기 있었던 HTML 태그 <marquee>에서 유래했습니다. 현대 모바일 개발에서 scrolling text는 플레이어, 뉴스 티커 및 상태 표시줄에서 사용됩니다. Apple Human Interface Guidelines 및 Google Material Design에 따르면, 마키는 생략 부호를 사용한 정적 자르기가 UX를 저하시키는 제한된 시나리오에서만 허용됩니다.
주요 사항
마키는 텍스트가 고정된 영역 내에서 수평으로 이동하며, 컨테이너에 맞지 않는 모든 콘텐츠를 사용자가 읽을 수 있도록 순환적으로 스크롤되는 UI 효과입니다. 텍스트의 일부를 숨기는 ellipsize와 달리, marquee는 전체 텍스트를 움직임으로 표시합니다.
마키라는 용어는 Netscape 및 Internet Explorer 브라우저 초기 버전에 추가된 HTML 태그 <marquee> 덕분에 확립되었습니다. 이 태그는 JavaScript나 CSS 없이 스크롤 텍스트를 만들 수 있었지만, HTML 표준의 일부가 된 적은 없습니다. 최신 브라우저는 하위 호환성을 위해 계속 지원하지만 사용이 권장되지 않습니다.
모바일 개발에서 마키는 컴팩트한 컨테이너의 긴 레이블 문제를 해결합니다: 플레이어의 트랙 이름, 환율 티커, TV 화면의 뉴스 티커 등입니다. 그러나 애니메이션으로 인해 접근성에 부담을 주고 사용자를 불편하게 할 수 있으므로 선택적으로 사용됩니다.
Nielsen Norman Group 연구(2023)에 따르면, 움직이는 텍스트는 읽기 속도를 15-20% 감소시키고 인지 부하를 증가시킵니다. 정적 자르기(ellipsize)가 정말로 허용되지 않는 경우에만 마키를 사용하십시오.
Android에서는 TextView의 내장 메커니즘을 통해 android:ellipsize="marquee" 속성으로 마키가 구현됩니다. 이는 텍스트를 자르지 않고 스크롤을 애니메이션화하는 유일한 ellipsize 모드입니다.
마키를 활성화하려면 singleLine="true"(또는 maxLines="1"), ellipsize="marquee", 코드에서 setSelected(true)의 세 가지 조건을 충족해야 합니다. setSelected()를 호출하지 않으면 애니메이션이 시작되지 않습니다.
<!-- XML layout -->
<TextView
android:id="@+id/marquee_text"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever" />
// Kotlin: activate marquee
textView.setSelected(true)
textView.isSingleLine = true
marqueeRepeatLimit 속성은 애니메이션 반복 횟수를 결정합니다: marquee_forever(무한) 또는 특정 숫자입니다. 기본값은 marquee_forever입니다. 텍스트가 컨테이너 너비보다 짧으면 애니메이션이 시작되지 않습니다 — TextView는 정적 텍스트를 표시합니다.
중요한 특징: setSelected(true)는 TextView가 포커스를 가지거나 선택 모드에 있을 때만 작동합니다. RecyclerView에서는 문제가 발생할 수 있습니다 — 애니메이션이 하나의 항목에 대해서만 시작됩니다. 해결책: isFocused()를 재정의하고 항상 true를 반환하는 사용자 정의 MarqueeTextView를 만듭니다.
Android 9(API 28)부터 Google은 마키 동작을 개선했습니다 — 애니메이션이 더 부드러워지고 목록 스크롤 시 멈추지 않습니다. 그러나 오래된 기기에서는 특히 높은 픽셀 밀도에서 마키가 떨릴 수 있습니다.
iOS에는 android:ellipsize="marquee"의 내장된 동등 기능이 없습니다. 마키는 Core Animation 또는 SwiftUI 애니메이션을 사용하여 수동으로 구현됩니다. 가장 일반적인 접근 방식은 클리핑 컨테이너와 함께 layer.position을 애니메이션화하는 것입니다.
UIKit에서 마키는 clipsToBounds = true인 UILabel 컨테이너, 텍스트 자체, 그리고 X축을 따라 텍스트를 무한 반복으로 이동하고 autoreverses = false로 설정하여 한 방향으로 스크롤하고 다시 점프하는 CABasicAnimation의 세 가지 요소로 구축됩니다.
func startMarquee(for label: UILabel) {
label.clipsToBounds = true
label.numberOfLines = 1
let textWidth = (label.text as? NSString)?
.size(withAttributes: [.font: label.font as any]).width ?? 0
guard textWidth > label.bounds.width else { return }
let animation = CABasicAnimation(keyPath: "position.x")
animation.fromValue = label.layer.position.x + label.bounds.width / 2
animation.toValue = label.layer.position.x - textWidth - label.bounds.width / 2
animation.duration = CFTimeInterval(textWidth / 60)
animation.repeatCount = .infinity
animation.autoreverses = false
label.layer.add(animation, forKey: "marquee")
}
duration = textWidth / 60 계산은 약 60픽셀/초의 속도를 제공합니다 — 편안한 읽기 속도입니다. 속도가 120 px/s를 초과하면 텍스트를 읽을 수 없게 됩니다. 30 px/s 미만이면 애니메이션이 느리게 보입니다. 특정 시나리오에 맞게 속도를 조정하십시오.
SwiftUI에서는 마키가 더 간단하게 구현됩니다 — 반복 가능한 애니메이션과 함께 offset() 수정자와 .animation(.linear(duration:).repeatForever(autoreverses: false))를 사용합니다. SwiftUI는 애니메이션 라이프사이클을 자동으로 관리하지만 텍스트 너비를 결정하기 위해 GeometryReader가 필요합니다.
웹에서 더 이상 사용되지 않는 <marquee> 태그는 권장되지 않지만, 브라우저는 계속 지원합니다. 마키의 현대적인 구현은 키프레임과 transform: translateX를 사용한 CSS 애니메이션입니다.
CSS 접근 방식의 장점: 속도, 일시 중지 및 애니메이션 곡선에 대한 완전한 제어, 접근성을 위한 prefers-reduced-motion 지원, 의미적으로 부적절한 태그가 없음.
.marquee {
overflow: hidden;
white-space: nowrap;
width: 300px;
}
.marquee-content {
display: inline-block;
animation: marquee 10s linear infinite;
}
@keyframes marquee {
0% { transform: translateX(0); }
100% { transform: translateX(-100%); }
}
/* Pause on hover */
.marquee-content:hover {
animation-play-state: paused;
}
핵심 요령: 콘텐츠에 display: inline-block을 사용하면 요소의 너비가 텍스트 너비와 같아지고, translateX(-100%)가 정확히 그 너비만큼 왼쪽으로 이동합니다. 콘텐츠가 display: block이면 translateX(-100%)는 텍스트 너비가 아닌 컨테이너 너비와 같아져 애니메이션이 깨집니다.
접근성을 위해 @media (prefers-reduced-motion: reduce) { .marquee-content { animation: none; } }를 추가하십시오 — 전정 장애가 있는 사용자는 스크롤 기능이 있는 정적 텍스트를 볼 수 있습니다.
마키는 사용자가 컨테이너를 직접 확장할 수 없고 전체 텍스트가 매우 중요한 시나리오에서 정당화됩니다: 잠금 화면의 플레이어에서 현재 트랙, TV 패널의 뉴스 티커, 라이드헤일링 앱의 마키(주문 상태).
마키는 테이블, 목록, 양식 및 대화상자에서 사용해서는 안 됩니다 — 사용자가 인터페이스와 적극적으로 상호 작용하는 모든 곳입니다. 애니메이션은 목표에서 주의를 분산시키고 요소 검색 시간을 증가시키며 불편을 초래합니다. WebAIM(2024)에 따르면, 전정 장애가 있는 사용자 3명 중 1명이 애니메이션 텍스트를 볼 때 불편함을 보고합니다.
경험 법칙: 텍스트를 줄이거나 다음 줄로 감쌀 수 있다면 그렇게 하십시오. 마키는 마지막 수단이며, 긴 레이블에 대한 보편적인 솔루션이 아닙니다.
마키는 접근성 관점에서 가장 문제가 되는 UI 애니메이션 중 하나입니다. 움직이는 텍스트는 전정 장애가 있는 사람들에게 어지러움, 메스꺼움 및 방향 감각 상실을 유발할 수 있습니다. WCAG 2.2에 따르면, 5초 이상 지속되는 애니메이션에는 일시 중지 또는 비활성화 메커니즘이 있어야 합니다.
iOS에서는 UIAccessibility.isReduceMotionEnabled 속성을 사용하여 시스템 설정을 확인합니다. reduceMotion이 활성화된 경우 — 애니메이션을 시작하지 말고 ellipsize와 함께 정적 텍스트를 표시합니다.
Android에서는 Settings.Global.ANIMATOR_DURATION_SCALE을 통해 시스템 애니메이션을 확인합니다. 글로벌 애니메이션이 비활성화된 경우(duration scale = 0) 마키를 활성화해서는 안 됩니다.
웹에서는 prefers-reduced-motion 미디어 기능을 사용합니다: @media (prefers-reduced-motion: reduce) { .marquee-content { animation: none; } }. 이는 모든 최신 브라우저에서 지원되는 표준 CSS 기능입니다. DevTools에서 확인하십시오: prefers-reduced-motion 에뮬레이션을 활성화하고 애니메이션이 꺼지는지 확인합니다.
자주 묻는 질문
Ellipsize는 텍스트를 자르고 생략 부호를 추가하여 콘텐츠의 일부를 숨깁니다. Marquee는 전체 텍스트를 움직임으로 스크롤합니다. 선택은 우선순위에 따라 달라집니다: 모든 것을 표시(marquee)하거나 정적 표시 유지(ellipsize).
내장된 android:ellipsize="marquee"는 속도를 변경할 수 없습니다 — 시스템 ScrollSpeed에 의해 결정됩니다. 사용자 정의 속도를 위해 Animation 또는 ValueAnimator를 수동으로 사용하십시오.
뷰 재사용으로 인해 setSelected(true)가 재설정됩니다. 해결책: isFocused()를 재정의하고 항상 true를 반환하는 사용자 정의 MarqueeTextView를 만들거나 Adapter.onViewAttachedToWindow()에서 사용자 정의 애니메이션을 사용합니다.
네, translateX 대신 translateY를 사용하십시오. 세로 마키는 크레딧 또는 세로 피드의 뉴스 티커에 사용됩니다. 원리는 동일하지만 애니메이션 축이 변경됩니다.
CSS: :hover { animation-play-state: paused; }. Android: OnFocusChangeListener를 통해 포커스를 추적하고 setSelected(false)를 재설정합니다. iOS: layer.removeAnimation(forKey:)을 통해 애니메이션을 제거합니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.