[Javascript] 멀티라인 속보형 무한 롤링 텍스트 배너 만들기
멀티라인 무한 롤링 배너에 앞서 한 줄로 동작하는 무한 롤링 텍스트 배너를 만드는 방법을 먼저 학습하는 것을 추천합니다. 기본적인 구조가 유사하고, 구현이 더 쉽기 때문에 개념을 이해하는데 더 도움이 됩니다.
> 속보형 무한 롤링 텍스트 배너 만들기
먼저 한 줄 롤링 배너에서는 기본적인 CSS 클래스 초기화 작업이 되어있는 HTML을 생성했지만, 멀티라인 롤링 배너에서는 조금 더 진보해서 자바스크립트로 초기화 CSS 클래스를 동적으로 생성합니다.
따라서 조금 더 정리되어 있고, 깔끔한 HTML 코드만 있으면 멀티라인 텍스트 롤링 배너를 생성할 수 있습니다.
최종적으로는 롤링 배너로 표시하는 텍스트의 행 개수를 자바스크립트 설정 변수로 자유롭게 변경할 수 있도록 해서 다양한 적용 환경에 바로바로 적용할 수 있도록 기능을 확장합니다.
1. HTML코드와 기본 레이아웃 CSS
먼저 HTML 코드는 다음과 같습니다.
<div class="rollingbanner">
<div class="title">속보 > </div>
<div class="wrap">
<ul>
<li><a href="#">얼어붙은 투심에…현대엔지니어링 상장 철회</a></li>
<li><a href="#">노바백스 백신 2월중순부터 접종</a></li>
<li><a href="#">얼어붙은 투심에…현대엔지니어링 상장 철회</a></li>
<li><a href="#">"일본 정부, 사도광산 세계유산 추천 방침 굳혀, 일본과 갈등 첨예화 예상"</a></li>
<li><a href="#">"파운드리 나노기술 경쟁 韓·대만 격전에 日 참전"</a></li>
<li><a href="#">"공법변경 구조검토 요구, 현산 측이 묵살했다"</a></li>
<li><a href="#">"미국서 ‘또’ 총기 난사…3명 사망, 용의자 수배 중"</a></li>
<li><a href="#">“설마했는데 푸틴 큰일났다”…‘중대결단’ 내린 미국 독일</a></li>
</ul>
</div>
</div>
초기 롤링 기능 구현을 위해 배너 뷰포트 영역에 표시하는 행 수를 4개로 고정합니다.
행 높이는 32px, 그리고 전체 텍스트 배너 개수는 동적으로 얻어옵니다.
CSS는 다음과 같이 작성합니다.
레이아웃 부분은 한 행으로 구혔했던 롤링 배너와 동일합니다.
높이값은 한 행을 32px로 했고, 상단의 15px의 패딩 여백이 더 있으므로 32px * 표시 행수 + 15px가 레이아웃 영역 높이가 됩니다. 하단 여백은 포함할 필요가 없습니다. 배너 텍스트가 하단 여백 없이 표시되어야 아래쪽으로 이동하면서 바로 텍스트가 감추어지기 때문에 하단에 여백이 있으면 안 됩니다.
html, body{
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
.container{
display: flex;
height: 100%;
justify-content: center;
align-items: center;
}
/* 배너 컨테이너 */
.rollingbanner{
position: relative;
width: 380px;
height: 143px; /* 뷰포트 높이 : 32px * 4 + 15px */
font-size: .875rem;
letter-spacing: -1px;
padding: 15px 15px;
box-sizing: border-box;
background-color: #f0f0f0;
border-radius: 16px;
}
/* 타이틀 */
.rollingbanner > .title{
font-weight: bold;
float: left;
padding-right: 10px;
}
/* 롤링 배너 */
.rollingbanner > .wrap{
position: relative;
width: auto;
height: 100%;
box-sizing: border-box;
overflow: hidden;
}
.rollingbanner ul{
list-style: none;
}
.rollingbanner li{
position: absolute;
left: 0;
top: -32px; /* 기본 위치는 배너 뷰포트 상단에 모두 위치하도록 해서 안보이게 함. */
}
/* 멀티라인 행별 위치 */
.rollingbanner li.prev{
top: 128px; /* 배너 뷰포트 표시 배너 행수를 4로 했을 때 바로 이전 감추어진 롤링 배너 최종 위치 */
transition: top 0.5s ease;
}
.rollingbanner li.next{/* 다음 표시할 배너 행 표시용 */
}
.rollingbanner li.current{
top: -32px; /* 현재 표시되는 배너들의 기본 위치 */
transition: top .5s ease;
}
/* 최대 6개까지 현재 뷰포트에 표시하는 배너들의 순서별 위치*/
.rollingbanner li.current.line0{
top: 0;
}
.rollingbanner li.current.line1{
top: 32px;
}
.rollingbanner li.current.line2{
top: 64px;
}
.rollingbanner li.current.line3{
top: 96px;
}
.rollingbanner li.current.line4{
top: 128px;
}
.rollingbanner li.current.line5{
top: 160px;
}
.rollingbanner a{
display: block;
display: -webkit-box;
text-decoration: none;
-webkit-line-clamp: 1;
-webkit-box-orient:vertical;
overflow: hidden;
color: #000;
}
한 행을 32px로 하고 배너 뷰포트에 4개의 배너 텍스트가 표시되도록 고정했을 때의 CSS입니다. 변수 값으로 동적으로 조정할 수 있도록 변경하므로 고정 뷰포트 표시 개수일 때 실제 값이 어떻게 설정되는지를 보면 됩니다.
텍스트 롤링 배너를 구현하는 방법은 몇 가지가 있는데, 여기서는 표시되는 각 표시 텍스트 행의 위치를 위에서부터 고정 위치로 설정하고(line1, line2 ... ) 클래스를 HTML 태그에 순차적으로 변경 적용해서 텍스트 행들이 아래로 애니메이션 되면서 이동하도록 구현합니다.
위 CSS는 뷰포트 표시 텍스트 배너 개수가 최대 6개(최소 1개)까지 가변으로 조정할 수 있도록 하기 위해 "line5"까지 클래스가 정의되어 있습니다.
멀티라인 텍스트 배너가 동적으로 구현되는 원리는 다음과 같습니다.
자바스크립트로는 적용된 클래스만 순차적으로 다음 HTML 태그에 옮겨서 적용하면 되기 때문에 개념적으로 단순합니다.
작성한 CSS가 정상 동작하는지 확인해 보려면 HTML 태그에 다음처럼 클래스를 추가해 주면 됩니다.
<ul>
<li class="current line0"><a href="#">얼어붙은 투심에…현대엔지니어링 상장 철회</a></li>
<li class="current line1"><a href="#">노바백스 백신 2월중순부터 접종</a></li>
<li class="current line2"><a href="#">얼어붙은 투심에…현대엔지니어링 상장 철회</a></li>
<li class="current line3"><a href="#">"일본 정부, 사도광산 세계유산 추천 방침 굳혀, 일본과 갈등 첨예화 예상"</a></li>
<li class="next"><a href="#">"파운드리 나노기술 경쟁 韓·대만 격전에 日 참전"</a></li>
<li><a href="#">"공법변경 구조검토 요구, 현산 측이 묵살했다"</a></li>
<li><a href="#">"미국서 ‘또’ 총기 난사…3명 사망, 용의자 수배 중"</a></li>
<li><a href="#">“설마했는데 푸틴 큰일났다”…‘중대결단’ 내린 미국 독일</a></li>
</ul>
CSS만 적용한 HTML 코드는 다음과 같이 표현됩니다.
2. 변수를 이용한 가변 레이아웃 구현
CSS에 적용한 배너 텍스트 행별 위치와 높이 값 정보를 CSS 변수로 일괄 관리할 수 있도록 변숫값 계산으로 변경합니다.
CSS 변수는 뒤에서 자바스크립트로 CSS 변수를 정의하는 방식으로 다시 변경해서 최종적으로는 자바스크립트로 모든 환경 설정을 하게 됩니다.
CSS 변수는 행의 높이, 그리고 뷰포트에 표시할 행 수 두 개를 정의합니다.
:root{
--lineheight:32px;
--showlines:4;
}
두 변수를 사용해서 롤링 배너 레이아웃 높이, 각 배너 텍스트의 위치를 계산식으로 표현합니다.
html, body{
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
.container{
display: flex;
height: 100%;
justify-content: center;
align-items: center;
}
/* 배너 컨테이너 */
.rollingbanner{
position: relative;
width: 380px;
height: calc(var(--lineheight)*var(--showlines)+15px); /* 레이아웃 높이 */
font-size: .875rem;
letter-spacing: -1px;
padding: 15px 15px;
box-sizing: border-box;
background-color: #f0f0f0;
border-radius: 16px;
}
/* 타이틀 */
.rollingbanner > .title{
font-weight: bold;
float: left;
padding-right: 10px;
}
/* 롤링 배너 */
.rollingbanner > .wrap{
position: relative;
width: auto;
height: 100%;
box-sizing: border-box;
overflow: hidden;
}
.rollingbanner ul{
list-style: none;
}
.rollingbanner li{
position: absolute;
left: 0;
top: calc(var(--lineheight)*-1); /* 기본 위치는 배너 뷰포트 상단에 모두 위치하도록 해서 안보이게 함. */
}
/* 멀티라인 행별 위치 */
.rollingbanner li.prev{
top: calc(var(--lineheight)*var(--showlines)); /* 마지막으로 이동해서 감추어진 롤링 배너 텍스트의 최종 위치 */
transition: top 0.5s ease;
}
.rollingbanner li.next{/* 다음 표시할 배너 행 표시용 */
}
.rollingbanner li.current{
top: calc(var(--lineheight)*-1); /* 현재 표시되는 배너들의 기본 위치 */
transition: top .5s ease;
}
/* 최대 6개까지 현재 뷰포트에 표시하는 배너들의 순서별 위치*/
.rollingbanner li.current.line0{
top: 0;
}
.rollingbanner li.current.line1{
top: var(--lineheight);
}
.rollingbanner li.current.line2{
top: calc(var(--lineheight)*2);
}
.rollingbanner li.current.line3{
top: calc(var(--lineheight)*3);
}
.rollingbanner li.current.line4{
top: calc(var(--lineheight)*4);
}
.rollingbanner li.current.line5{
top: calc(var(--lineheight)*5);
}
.rollingbanner a{
display: block;
display: -webkit-box;
text-decoration: none;
-webkit-line-clamp: 1;
-webkit-box-orient:vertical;
overflow: hidden;
color: #000;
}
CSS는 모두 완성되었습니다.
이제 자바스크립트로 배너 텍스트 행들을 롤링시키면 됩니다.
먼저 루트 도큐먼트 엘리먼트에 선언한 CSS 변수 2개(--lineheight, --showlines)를 다음 자바스크립트 코드로 대체합니다.
이제 자바스크립트 변수로 CSS의 변수까지 연동되며, showlines, lineheight 두 변수로 롤링 배너의 뷰포트 높이와 표시 배너 개수를 제어할 수 있게 되었습니다.
let showlines = 4, lineheight = 32, linecount = 0; //라인 행수와 라인 1개 높이 환경 설정 값
document.documentElement.style.setProperty('--lineheight', lineheight+'px');//CSS 변수 라인 높이 값 설정
document.documentElement.style.setProperty('--showlines', showlines);//CSS 변수 라인 높이 값 설정
그리고 CSS로 변수를 선언하는 데 사용한 ":root" 클래스는 삭제합니다.
자바스크립트에서 사용하는 변수 3개 중 초기화를 하지 않은 "linecount" 변수 값(롤링 배너 전체 개수)을 쿼리선택자로 얻습니다. 그리고 사용자 실수를 방지하기 위해 설정한 뷰포트 표시 행수 변수 값이 6보다 크면 6으로 제한을 해서 스크립트 코드가 에러가 발생하는 것을 막습니다.
6보다 큰 뷰포트 배너 행수를 사용하고 싶으면 앞서 작성한 CSS에서 ".rollingbanner li.current.line*" 클래스를 더 추가해 주고 최대 행수 제한을 변경하면 됩니다.
document.addEventListener('DOMContentLoaded', ()=>{
linecount = document.querySelectorAll('.rollingbanner ul li')?.length ?? 0
showlines = (showlines > 6 ? 6:showlines)
})
3. HTML 배너 초기 위치 클래스 적용
HTML 태그에 초기 위치를 잡는 클래스를 적용해야 합니다.
앞서 CSS가 정상 적용되는지 테스트 용으로 HTML 태그에 적용해 봤던 클래스들을 자바스크립트 변수 값을 기준으로 자동 적용하는 코드입니다.
롤링 배너 뷰포트 안에 표시되는 행은 "current", "line*" 2개의 클래스를 가지게 됩니다. "current" 클래스는 뷰포트 안에 표시되는 배너 행의 공통 적용 속성들을 적용하는 클래스이고, "line*"(line0, line1, line2 ...) 클래스는 행별 top 위치를 정하는 클래스입니다.
"line*" 클래스 명 끝에 붙는 숫자는 0부터 시작합니다.
자바스크립트의 인덱스 값과 일치시키기 위해 0부터 값을 사용하고 있고, 코드의 간결함을 위한 것입니다.
여기서는 뷰포트 배너 표시 행수가 4이므로 querySelectorAll() 쿼리선택자로 선택한 전체 배너 텍스트 행들 중 앞에서부터 4개만 line0, line1, line2, line3 클래스가 순차적용됩니다.
그리고 line3 클래스를 적용한 바로 다음 텍스트 행에는 "next" 클래스를 적용해서 다음번 표시될 텍스트 행임을 표시합니다. 앞서 CSS에 정의한 ".next" 클래스를 보면 배너 뷰포트 영역 바로 위에 위치하도록 top 속성 값이 지정(-32px)되어 있기 때문에 "next" -> "current line0"로 클래스를 변경하면 뷰포트 상단 외곽에서부터 배너 맨 윗 행으로 배너가 이동해서 내려오게 됩니다.
document.querySelectorAll('.rollingbanner ul li').forEach((line,idx)=>{
if(idx < showlines){
line.classList.add('current','line'+((showlines > linecount ? linecount:showlines)-idx-1))
}else if(idx == showlines){
line.classList.add('next')
}
})
4. 배너 롤링 인터벌 이벤트 핸들러 초기화
뷰포트 영역에 표시할 행수보다 전체 배너 행수가 크면 롤링이 되도록 해야 합니다. 전체 행수가 뷰포트 표시 행수보다 같거나 작으면 이미 전체 배너가 화면 안에 표시되고 있기 때문에 롤링을 할 필요가 없습니다.
인터벌 메서드로 일정 시간 간격으로 콜백 함수(rollingCallback)를 호출하도록 초기화합니다.
시간은 3초(3000)로 설정했습니다.
실제 배너 행의 롤링은 콜백 함수에서 모두 이루어집니다.
if(linecount > showlines){
var interval = window.setInterval(rollingCallback, 3000);
}
여기까지 작성하면 일단 배너 초기화를 완료되었고, 콜백 함수만 작성해서 HTML 태그에 추가한 클래스들을 하나씩 순차적으로 밀어서 적용하면 배너 텍스트들이 3초 간격으로 아래쪽으로 무한 롤링을 하게 됩니다.
인터벌 메서드 초기화 앞에는 추가로 작성할 초기화 코드가 하나 더 있습니다.
뷰포트 마지막 행이 밑으로 내려가서 감추어진 직후에 해당 행을 뷰포트 상단 바깥으로 이동시켜서 밑으로 이동할 수 있도록 위치를 초기화해주어야 합니다.
뒤에서 자세히 설명합니다.
5. 콜백 함수로 배너 롤링 애니메이션 구현
앞서 배너 롤링을 설명한 그림에서 클래스들이 어떻게 변경되는지 봤으므로 그림처럼 클래스들을 순차적으로 변경해 주면 됩니다.
콜백 함수는 2 부분으로 구성되어 있습니다.
앞쪽의 for 루프문은 "current line*" 클래스를 순차적으로 하나씩 숫자를 증가시키는 루프문입니다.
마지막 배너 텍스트 행(뷰포트 표시 행 개수가 4개이면 "current line3")은 클래스를 "prev"로 변경해서 뷰포트 아래쪽 바깥으로 이동시켜서 표시하는 배너에서 제외합니다.
뒤쪽의 조건문은 "next" 클래스를 가지고 있는 행을 쿼리선택자로 선택해서 "next" -> "current line0" 으로 클래스를 변경해서 뷰포트 상단 바깥쪽에서 첫 번째 행(가장 위쪽의 행)으로 표시합니다.(맨 마지막 두 행)
//롤링 배너
function rollingCallback(){
//배너 행 CSS를 하나씩 밀어서 푸시
for(let i = showlines-1;0<=i;i--){
let current = document.querySelector('.rollingbanner li.current.line'+i);
if(current){
current.classList.remove('line'+i)
if(i < showlines-1){
//나머지 current -> current+1
current.classList.add('line'+(i+1))
}else{
//마지막 current -> prev
document.querySelector('.rollingbanner .prev')?.classList.remove('prev');
current.classList.remove('current','line'+i)
current.classList.add('prev')
}
}
};
//다음 행 위치를 지정
//.next -> .current .line0
let next = document.querySelector('.rollingbanner .next');
//.next 다음 요소가 널인지 체크 - 다음 행이 첫 행인지 체크
if(next){
if(next.nextElementSibling == null){//다음 행이 첫 행
document.querySelector('.rollingbanner ul li:first-child').classList.add('next');
}else{//
//목록 처음 요소를 다음 요소로 선택
next.nextElementSibling.classList.add('next');
}
//next -> current0
next.classList.add('current','line0')
next.classList.remove('next');
}
}
if 조건문에 주의할 내용이 있습니다.
다음 조건문으로 체크를 하는 이유는 다음번 표시할 요소를 현재 "next" 클래스를 가진 배너 행의 다음 행으로 선택해야 하는데, 마지막 HTML 요소(li)인 경우 "next" 클래스를 적용할 다음 요소를 선택할 수 없기 때문에 목록의 첫 번째 요소를 선택하도록 구분해서 쿼리선택자를 적용해야 하기 때문입니다.
if(next.nextElementSibling == null)
6. 클래스 중첩으로 인한 애니메이션 이상 해결
전체 배너 행 개수가 뷰포트 표시 배너 개수 보다 1개 많은 경우(전체 배너 행 개수 5개, 뷰포트 표시 개수 4개), 롤링 배너는 동작하지만 "prev"와 "next" 클래스가 같은 배너 행에 중첩되면서 뷰포트 상단에서 대기해야 할 "next" 클래스를 가진 배너 행이 뷰포트 하단에 머물게 되는 문제가 발생합니다.
"prev" 클래스를 가지고 있으므로 뷰포트 하단으로 사라진 후 해당 위치에 있는 것이 정상이지만, 클래스가 중첩되면서 "next" 클래스를 가진 배너행이 뷰포트 하단 바깥에서 뷰포트 첫 번째 배너해 위치로 이동하는 문제가 발생합니다.
이 문제를 해결하려면 "prev" 클래스가 적용되어 마지막 배너 행이 뷰포트 하단으로 사라지는 동작이 완료된 후 "prev" 클래스를 삭제해서 뷰포트 상단 바깥으로 점프해서 대기를 하도록 처리해야 합니다.
앞서 4번에서 작성한 인터벌 이벤트 핸들러 앞에 "prev" 클래스 애니메이션 트랜지션이 종료된 직후 발생하는 이벤트를 처리하는 핸들러를 추가해 줍니다.
if(linecount > showlines){
// "prev" 클래스 삭제 이벤트 핸들러
document.querySelector('.rollingbanner ul').addEventListener('transitionend', (event) => {
if(event.target.classList.contains('prev')){
event.target.classList.remove('prev')
}
});
var interval = window.setInterval(rollingCallback, 3000);
}
이벤트 핸들러를 추가할 때 중요한 점은 이벤트 핸들러를 추가하는 요소가 배너 행이 아니라 배너 전체를 감싸고 있는 래퍼(UL) 태그여야 하는 것입니다. 배너 개별 행에 추가를 하려면 이벤트 핸들러를 모든 배너행에 추가해야 하기 때문에 코드가 복잡해지고 많은 이벤트핸들러 등록으로 인해 배너가 느려지게 됩니다.
래퍼에 이벤트핸들러를 추가해서 발생한 이벤트에서 "prev" 클래스를 가진 배너행인지를 구분해서 처리를 하는 것이 조금 더 효율적인 방법입니다.
7. 뷰포트에 표시할 배너 개수 변경
소스 코드에서 showlines = 4로 설정한 행을 찾아서 4 대신 다른 값을 입력(1~6)하면 표시하는 배너 개수가 변경됩니다.