🐨CoalaCoding
DocsExamplesTry itBoardB반B반
🐨CoalaCoding

개발자를 위한 한국어 웹 기술 문서

문서

  • JavaScript
  • Web Publishing
  • React
  • Python

커뮤니티

  • 게시판
  • 예제 모음
  • Try it 에디터

기타

  • GitHub
  • 관리자
© 2026 CoalaCoding. All rights reserved.
  • 스킬바 UI
  • gsap
  • GSAP 플러그인 종류와 기초 애니메이션
  • GSAP 플러그인
  1. 홈
  2. 문서
  3. HTML & CSS
  4. Animation & GSAP
  5. 스킬바 UI

스킬바 UI

스크롤 위치에 따라 막대형, 원형 스킬바를 애니메이션하는 방법을 학습한다.

코드 블록의 Try it Yourself 버튼으로 직접 실행할 수 있다.

구문

스킬바 UI

스킬바는 포트폴리오 사이트에서 기술 숙련도를 시각적으로 보여주는 UI 요소이다. 스크롤 위치에 따라 막대나 원이 채워지는 애니메이션을 구현한다.


1. 막대형 스킬바

스크롤이 특정 위치에 도달하면 막대가 늘어나면서 퍼센트 숫자가 올라가는 애니메이션이다.

Note: jQuery 3 기반의 막대형 스킬바를 만들어줘. 조건은 다음과 같아. 1) 프로그레스 바 3개를 세로로 배치하고, 각각 목표 퍼센트를 data-rate 속성으로 지정한다(30%, 90%, 60%). 2) 스크롤이 애니메이션 영역에 도달하면 막대의 width가 0%에서 목표 퍼센트까지 2500ms 동안 늘어난다. 3) 동시에 퍼센트 숫자가 0부터 목표값까지 카운트업된다. 4) 애니메이션은 1회만 실행된다. 5) 이벤트 바인딩은 .on('scroll')을 사용한다.

HTML

<div class="content">contents</div>
<div class="animation">
  <div class="progress-bar">
    <div class="bar"></div>
    <div class="rate" data-rate="30"></div>
  </div>
  <div class="progress-bar">
    <div class="bar"></div>
    <div class="rate" data-rate="90"></div>
  </div>
  <div class="progress-bar">
    <div class="bar"></div>
    <div class="rate" data-rate="60"></div>
  </div>
</div>
<div class="content">contents</div>
줄설명
1스크롤 확인용 콘텐츠 영역이다
2애니메이션 영역의 시작이다. 이 요소의 위치를 기준으로 스크롤을 감지한다
3하나의 프로그레스 바 묶음이다
4실제로 늘어나는 색상 막대이다
5퍼센트 숫자를 표시하는 영역이다. data-rate에 목표 퍼센트 값을 지정한다

CSS

.progress-bar {
  position: relative;
  width: 960px;
  height: 30px;
  margin: 3em auto;
  border: 1px solid green;
}
.progress-bar .bar {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 0;
  background: green;
}
.progress-bar .rate {
  position: absolute;
  top: 0;
  right: 15px;
  bottom: 0;
  line-height: 30px;
  font-size: 1.2em;
  color: green;
}
.content {
  height: 800px;
  font-size: 3em;
}
줄설명
1~7.progress-bar를 position: relative로 설정하여 내부 요소의 기준점으로 만든다
8~15.bar는 position: absolute로 왼쪽에 붙이고, width: 0으로 시작한다. jQuery가 이 값을 늘린다
16~24.rate는 오른쪽에 배치하여 퍼센트 텍스트를 표시한다. line-height로 수직 중앙 정렬한다
25~28.content에 height: 800px을 주어 스크롤 공간을 확보한다

JS

$(function () {
  var wrap = $('.progress-bar');
  var ost = $('.animation').offset().top - 600;
  var isAni = false;

  $(window).on('scroll', function () {
    if ($(window).scrollTop() >= ost && !isAni) {
      runAnimation();
    }
  });

  function runAnimation() {
    wrap.each(function () {
      var $this = $(this);
      var bar = $this.find('.bar');
      var txt = $this.find('.rate');
      var rate = txt.attr('data-rate');

      bar.animate({ width: rate + '%' }, 2500);

      $({ rate: 0 }).animate(
        { rate: rate },
        {
          duration: 2000,
          progress: function () {
            var now = this.rate;
            txt.text(Math.floor(now) + '%');
          },
          complete: function () {
            isAni = true;
          },
        }
      );
    });
  }
});
줄설명
1$(function(){}) — 문서가 준비되면 실행하는 jQuery 단축 문법이다
2모든 .progress-bar 요소를 선택하여 wrap에 저장한다
3.animation 영역의 상단 위치에서 600px을 뺀 값을 트리거 지점으로 설정한다
4isAni — 애니메이션 실행 여부를 추적하는 플래그 변수이다
6~10.on('scroll') — 스크롤 이벤트를 등록한다. 현재 위치가 트리거 지점 이상이고 아직 실행 전이면 애니메이션을 시작한다
13.each() — 각 .progress-bar를 순회하며 개별 처리한다
17data-rate 속성에서 목표 퍼센트 값을 읽는다
19.bar의 width를 목표 퍼센트까지 2500ms 동안 애니메이션한다
21~33가상 객체 { rate: 0 }을 만들어 숫자를 0부터 목표값까지 증가시킨다
25~27progress — 매 프레임마다 호출되는 콜백이다. 현재 값을 내림 처리하여 텍스트로 표시한다
29~31complete — 완료 시 isAni를 true로 바꿔 재실행을 방지한다
jQuery .animate() 메서드란?

.animate()는 CSS 속성 값을 지정한 시간 동안 서서히 변경하는 jQuery 메서드이다.

$('선택자').animate({ 속성: 값 }, 시간);
줄설명
1첫 번째 인자는 변경할 CSS 속성과 목표 값이다. 두 번째 인자는 밀리초 단위의 소요 시간이다

숫자 값을 가진 CSS 속성(width, height, opacity 등)만 애니메이션할 수 있다.


1.1. 원형 스킬바

SVG의 circle 요소와 stroke-dashoffset 속성을 활용한 원형 프로그레스 바이다.

Note: jQuery 3 기반의 원형 스킬바를 만들어줘. 조건은 다음과 같아. 1) SVG circle 요소(반지름 100, 둘레 628)를 사용한 원형 프로그레스 바 4개를 가로로 배치한다. 2) 각 원의 중앙에 퍼센트 숫자를 표시하고, data-num 속성으로 목표값을 지정한다(20, 60, 80, 50). 3) stroke-dasharray와 stroke-dashoffset을 628로 설정하고, 스크롤 시 dashoffset을 줄여서 선이 그려지는 애니메이션을 구현한다. 4) 12시 방향부터 시작하도록 rotate(-90deg)를 적용한다. 5) 각 원마다 다른 stroke 색상을 지정한다. 6) hasClass로 중복 실행을 방지한다. 7) 이벤트 바인딩은 .on('scroll')을 사용한다.

HTML

<section>
  <h2>content</h2>
</section>
<div class="charts">
  <div class="chart">
    <h2 data-num="20">0</h2>
    <svg>
      <circle cx="110" cy="110" r="100"></circle>
    </svg>
  </div>
  <div class="chart">
    <h2 data-num="60">0</h2>
    <svg>
      <circle cx="110" cy="110" r="100"></circle>
    </svg>
  </div>
  <div class="chart">
    <h2 data-num="80">0</h2>
    <svg>
      <circle cx="110" cy="110" r="100"></circle>
    </svg>
  </div>
  <div class="chart">
    <h2 data-num="50">0</h2>
    <svg>
      <circle cx="110" cy="110" r="100"></circle>
    </svg>
  </div>
</div>
<section>
  <h2>content</h2>
</section>
줄설명
1~3스크롤 공간 확보용 섹션이다
4.charts — 원형 차트들을 감싸는 컨테이너이다
6data-num="20" — 목표 퍼센트 값이다. 초기 텍스트는 0이다
8circle — SVG 원 요소이다. cx, cy는 중심 좌표, r은 반지름이다

CSS

.charts {
  width: 80%;
  margin: 3rem auto;
  display: flex;
  justify-content: center;
}
.charts .chart {
  margin: 0 20px;
  position: relative;
}
.charts .chart h2 {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
}
.charts .chart svg {
  width: 220px;
  height: 220px;
}
circle {
  fill: #ffffff;
  stroke-width: 20;
  stroke-dasharray: 628;
  stroke-dashoffset: 628;
  transform: rotate(-90deg);
  transform-origin: 50% 50%;
  stroke-linecap: round;
}
.charts .chart:nth-child(1) circle { stroke: #ffc114; }
.charts .chart:nth-child(2) circle { stroke: #ff5248; }
.charts .chart:nth-child(3) circle { stroke: #19cdca; }
.charts .chart:nth-child(4) circle { stroke: #4e88e1; }
section {
  height: 100vh;
}
줄설명
1~6flexbox로 원형 차트들을 가로로 나란히 배치한다
11~17h2를 absolute + translate로 원의 정중앙에 배치한다
22~30stroke-dasharray: 628은 원 둘레(2πr = 2 × 3.14 × 100 ≈ 628)와 같다. stroke-dashoffset: 628이면 선이 완전히 숨겨진 상태이다
27rotate(-90deg) — 12시 방향부터 시작하도록 회전한다
31~34각 차트마다 다른 색상을 지정한다

JS

$(function () {
  var charts = $('.charts');
  var chart = $('.chart');
  var ost = chart.offset().top - 700;

  $(window).on('scroll', function () {
    var sct = $(this).scrollTop();
    if (sct >= ost) {
      if (!charts.hasClass('active')) {
        animateChart();
        charts.addClass('active');
      }
    }
  });

  function animateChart() {
    chart.each(function () {
      var item = $(this);
      var title = item.find('h2');
      var num = title.attr('data-num');
      var circle = item.find('circle');

      $({ rate: 0 }).animate(
        { rate: num },
        {
          duration: 1500,
          progress: function () {
            var now = this.rate;
            var offset = 630 - (630 * now) / 100;
            title.text(Math.floor(now));
            circle.css({ strokeDashoffset: offset });
          },
        }
      );
    });
  }
});
줄설명
4.chart의 상단 위치에서 700px을 뺀 값을 트리거 지점으로 설정한다
9.hasClass('active') — active 클래스 유무로 중복 실행을 방지한다
11애니메이션 실행 후 active 클래스를 추가한다
23~33가상 객체의 rate를 0부터 목표값까지 1500ms 동안 변화시킨다
29stroke-dashoffset 계산식: 630(전체 둘레)에서 진행률만큼 뺀다. 값이 줄어들수록 선이 드러난다
30현재 숫자를 정수로 변환하여 중앙 텍스트를 갱신한다
31계산된 offset 값을 circle의 strokeDashoffset에 반영한다
stroke-dasharray와 stroke-dashoffset이란?

SVG 선 애니메이션의 핵심 속성 2가지이다.

  • stroke-dasharray — 점선 패턴의 길이를 지정한다. 원 둘레(628)와 같은 값을 주면 하나의 긴 선이 된다
  • stroke-dashoffset — 점선의 시작 위치를 밀어낸다. 628이면 선이 보이지 않고, 0이면 완전히 보인다

이 두 속성의 조합으로 "선이 그려지는" 애니메이션을 구현한다.


1.2. SVG 라인형 스킬바

circle 대신 line 요소를 사용한 직선형 변형이다.

Note: jQuery 3 기반의 SVG 라인형 스킬바를 만들어줘. 조건은 다음과 같아. 1) SVG line 요소(x1=0, y1=0, x2=500, y2=0)를 사용한 직선형 프로그레스 바 3개를 가로로 배치한다. 2) stroke-dasharray와 stroke-dashoffset을 300으로 설정하고, 스크롤 시 dashoffset을 줄여서 선이 그려지는 애니메이션을 구현한다. 3) 각 라인 중앙에 퍼센트 숫자 카운트업을 표시한다. 4) 각 라인마다 다른 stroke 색상을 지정한다. 5) hasClass로 중복 실행을 방지한다. 6) $(function(){})로 감싸고, 이벤트 바인딩은 .on('scroll')을 사용한다.

HTML

<section>
  <h2>content</h2>
</section>
<div class="charts">
  <div class="chart">
    <h2 data-num="20">0</h2>
    <svg>
      <line x1="0" y1="0" x2="500" y2="0"></line>
    </svg>
  </div>
  <div class="chart">
    <h2 data-num="20">0</h2>
    <svg>
      <line x1="0" y1="0" x2="500" y2="0"></line>
    </svg>
  </div>
  <div class="chart">
    <h2 data-num="80">0</h2>
    <svg>
      <line x1="0" y1="0" x2="500" y2="0"></line>
    </svg>
  </div>
</div>
<section>
  <h2>content</h2>
</section>
줄설명
8line 요소는 시작점(x1, y1)에서 끝점(x2, y2)까지 직선을 그린다
6data-num — 목표 퍼센트 값이다. circle 예제와 동일한 방식이다

CSS

.charts {
  width: 80%;
  margin: 3rem auto;
  display: flex;
  justify-content: center;
}
.charts .chart {
  margin: 0 20px;
  position: relative;
}
.charts .chart h2 {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
}
line {
  stroke-width: 20;
  stroke-dasharray: 300;
  stroke-dashoffset: 300;
}
.charts .chart:nth-child(1) line { stroke: #ffc114; }
.charts .chart:nth-child(2) line { stroke: #ff5248; }
.charts .chart:nth-child(3) line { stroke: #19cdca; }
section {
  height: 100vh;
}
줄설명
18~22stroke-dasharray와 stroke-dashoffset을 300으로 설정한다. 원형과 같은 원리이지만 직선 길이에 맞춘 값이다
23~25각 라인마다 다른 색상을 지정한다

JS

$(function () {
  var charts = $('.charts');
  var chart = $('.chart');
  var ost = chart.offset().top - 700;

  $(window).on('scroll', function () {
    var sct = $(this).scrollTop();
    if (sct >= ost) {
      if (!charts.hasClass('active')) {
        animateChart();
        charts.addClass('active');
      }
    }
  });

  function animateChart() {
    chart.each(function () {
      var item = $(this);
      var title = item.find('h2');
      var num = title.attr('data-num');
      var el = item.find('line');

      $({ rate: 0 }).animate(
        { rate: num },
        {
          duration: 1500,
          progress: function () {
            var now = this.rate;
            var offset = 300 - (300 * now) / 100;
            title.text(Math.floor(now));
            el.css({ strokeDashoffset: offset });
          },
        }
      );
    });
  }
});
줄설명
1원본에는 $(function(){})가 없었으나, DOM 안전을 위해 감싸는 것이 올바르다
21circle 대신 line 요소를 찾는다
29계산식의 기준값이 630이 아닌 300이다. stroke-dasharray 값과 일치시켜야 한다

Warning: 원본 코드에서 라인형(1.2)의 JS는 $(function(){})로 감싸져 있지 않았다. 문서 로드 전에 DOM 요소를 찾으면 undefined 오류가 발생할 수 있으므로 반드시 감싸야 한다.


2. GSAP으로 원형 스킬바 만들기

GSAP과 ScrollTrigger를 사용하면 jQuery 없이도 스킬바 애니메이션을 구현할 수 있다.

Note: GSAP과 ScrollTrigger로 원형 스킬바를 만들어줘. 조건은 다음과 같아. 1) CSS conic-gradient와 CSS 변수 --p를 사용하여 원형 게이지를 구현한다. 2) gsap.to로 CSS 변수 --p를 0에서 목표 퍼센트까지 4초 동안 변경하여 게이지를 채운다. 3) ease는 expo.out을 사용한다. 4) 원 중앙의 숫자도 0부터 목표값까지 카운트업하되, modifiers 플러그인의 toFixed()로 소수점을 제거한다. 5) ScrollTrigger의 toggleActions를 play pause resume reset으로 설정하여 스크롤 위치에 따라 애니메이션을 제어한다. 6) timeline을 사용하여 게이지 채우기와 숫자 카운트를 동시에 실행한다.

2.1. CDN 로드

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>
줄설명
1GSAP 핵심 라이브러리를 불러온다
2ScrollTrigger 플러그인을 불러온다. 스크롤 위치 기반 애니메이션에 필요하다

2.2. HTML 구조

<div class="circular-pbar">
  <span class="circular-pbar-counter">0</span>
</div>
줄설명
1원형 프로그레스 바의 컨테이너이다. CSS의 conic-gradient로 원형 게이지를 만든다
2중앙에 표시할 숫자 영역이다. 초기값은 0이다

2.3. 기본 CSS

.circular-pbar {
  width: 200px;
  height: 200px;
  border-radius: 50%;
  background: conic-gradient(darkred 33%, 0, black);
  position: relative;
}
.circular-pbar-counter {
  position: absolute;
  font-size: 3em;
  color: white;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
줄설명
3border-radius: 50%로 정사각형을 원으로 만든다
5conic-gradient — 원뿔형 그라디언트이다. darkred가 33% 지점까지 채워지고 나머지는 black이다
8~14숫자를 absolute + translate로 원의 정중앙에 배치한다

2.4. CSS 변수로 애니메이션 준비

Tip: .circular-pbar에 CSS 지역변수 --p를 선언하고 0을 할당한다. 배경의 색상 위치에 이 변수를 넣으면 JS에서 변수 값만 바꿔도 게이지가 움직인다.

.circular-pbar {
  --p: 0;
  background: conic-gradient(#df3030 var(--p, 0), 0, #cacaca);
}
줄설명
2--p: 0 — CSS 사용자 정의 속성을 선언하고 초기값 0을 넣는다
3var(--p, 0) — --p 값을 그라디언트의 색상 위치로 사용한다. GSAP이 이 값을 변경하면 게이지가 채워진다

2.5. GSAP으로 게이지 애니메이션

Note: gsap.to를 사용하여 CSS 변수 --p의 값을 변경하면 원형 게이지가 채워진다.

gsap.to('.circular-pbar', {
  '--p': '33%',
  duration: 4,
  ease: 'expo.out',
});
줄설명
1gsap.to — 대상 요소를 지정한 속성 값까지 애니메이션한다
2'--p': '33%' — CSS 변수 --p를 0에서 33%까지 변경한다
3duration: 4 — 4초 동안 진행한다
4ease: 'expo.out' — 처음에 빠르고 끝에서 느려지는 가속 곡선이다

2.6. 숫자 카운트 + ScrollTrigger 적용

스크롤 위치에 따라 숫자도 함께 올라가도록 확장한다.

const circles = document.querySelectorAll('.circular-pbar');

circles.forEach((el) => {
  const counter = el.querySelector('.circular-pbar-counter');
  const tg = counter.textContent + '%';

  const tm = gsap.timeline({
    defaults: { duration: 4, ease: 'expo.out' },
    scrollTrigger: {
      trigger: el,
      toggleActions: 'play pause resume reset',
    },
  });

  tm.from(counter, {
    textContent: 0,
    modifiers: {
      textContent: (textContent) => {
        return textContent.toFixed();
      },
    },
  });

  tm.to(el, { '--p': tg }, 0);
});
줄설명
1모든 .circular-pbar 요소를 선택한다
3forEach로 각 원형 바를 순회한다
5counter 요소의 텍스트(목표 퍼센트 값)를 읽어 '%'와 결합한다. 이 값은 CSS 변수로 사용된다
7~8GSAP timeline을 생성하고 기본값으로 지속 시간 4초, 가속 곡선 expo.out을 할당한다
9~12ScrollTrigger를 연결한다. toggleActions는 순서대로 진입/이탈/재진입/재이탈 시 동작이다
15~22tm.from — textContent를 0부터 목표값까지 애니메이션한다
17~20modifiers — 속성 변경값을 실시간으로 가공하는 GSAP 내장 플러그인이다. .toFixed()로 소수점을 제거한다
24tm.to — CSS 변수 --p를 목표값까지 변경한다. 세 번째 인자 0은 timeline 시작과 동시에 실행한다는 의미이다
modifiers 플러그인이란?

modifiers는 GSAP에 기본 내장된 플러그인으로, 애니메이션 진행 중 특정 속성의 값을 매 프레임마다 가공할 수 있다.

  • 모든 애니메이션 속성에 사용 가능하다
  • 반드시 함수 형태로 작성해야 한다
  • 함수의 인자로 현재 값이 전달되고, 반환값이 실제 적용된다
modifiers: {
  textContent: (value) => {
    return Math.round(value);
  },
}
줄설명
2textContent 속성에 modifier를 적용한다. 매 프레임마다 이 함수가 호출된다
3Math.round로 반올림한 정수를 반환한다

목차

  • 구문