GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

pdf 미리보기 구현 9가지 — 스크롤로 페이지를 계산하는 pdf 뷰어 ui

스크롤 값이 페이지 번호를 만드는 걸까, 페이지 번호가 스크롤 값을 만드는 걸까 — 02번 확대 중심은 포인터 좌표로, 04번 다음 페이지는 손을 뗄 때의 속도로, 08번 다운로드 링은 받은 바이트 수로 답한다. pdf 미리보기 구현을 이 아홉 갈래로 나눠보면 답이 보인다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

pdf 미리보기 구현 9가지 — 스크롤과 확대를 계산으로 잇는 뷰어

아홉 항목은 전부 "지금 어디를 보고 있는가"를 하나의 숫자로 계산하고, 그 숫자로 다른 요소를 움직인다. 01·07·09 는 스크롤 위치를 페이지 번호로 바꾸고, 02·06 은 좌표와 각도를 화면 크기로 바꾸며, 04·05·08 은 속도와 바이트 수를 진행률로 바꾼다. 03 은 이 계산 축의 반대 방향이다 — 페이지 번호를 알고 스크롤 위치를 구한다. pdf 뷰어 ui 를 직접 만들 때 이 아홉 갈래 중 몇 개가 겹쳐 있는지 보면 뷰어가 왜 복잡해지는지 알 수 있다.

패턴별 코드

아래 코드는 계산이 몰려 있는 구간만 잘라 붙였고, 화면을 채우는 스타일·마크업 나머지는 데모 아래 압축 파일에 들어 있다. 페이지 내용은 전부 seedRand 로 만든 가짜 문서이고, 실제 PDF 파일을 읽지는 않는다.

01본문 스크롤 비율로 현재 페이지를 계산하는 썸네일 레일 동기화

각 페이지 요소가 뷰포트와 겹치는 비율을 매 프레임 계산해 가장 많이 보이는 페이지를 찾고, 그 인덱스가 바뀔 때만 썸네일 레일의 강조 칸과 배지 숫자를 갱신한다. 레일 자체는 강조된 칸이 가운데로 오도록 목표 위치를 향해 매 프레임 조금씩 따라가며, 사용자가 직접 조작하면 그 즉시 따라가는 비율이 1로 바뀐다. 01 항목은 누적면적 59.6%·강도 평균 39.5·23프레임 중 23프레임에서 움직였다.

본문 스크롤 비율로 현재 페이지를 계산하는 썸네일 레일 동기화
새 창에서 열기

01 · 본문 스크롤 비율로 현재 페이지를 계산하는 썸네일 레일 동기화 — 측정 누적면적 59.608% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

RAIL_LERPcomputeCurrentvisibleRatio
  function visibleRatio(el, viewTop, viewH) {
    var top = el.offsetTop, h = el.offsetHeight;
    var lo = Math.max(top, viewTop), hi = Math.min(top + h, viewTop + viewH);
    return Math.max(0, hi - lo) / h;
  }
  function computeCurrent() {
    var viewTop = doc.scrollTop, viewH = doc.clientHeight, best = 0, bestR = -1;
    for (var i = 0; i < docEls.length; i += 1) {
      var r = visibleRatio(docEls[i], viewTop, viewH);
      if (r > bestR) { bestR = r; best = i; }
    }
    if (best !== current) {
      current = best;
      for (var j = 0; j < thumbEls.length; j += 1) thumbEls[j].classList.toggle('pvt__thumb--active', j === current);
      badge.textContent = (current + 1) + ' / ' + pages.length;
    }
  }

02포인터 좌표를 기준으로 확대 중심을 계산하는 초점 줌

휠이 일어난 지점의 화면 좌표를 뷰어 안쪽 요소 기준 비율(0~1)로 바꿔 두고, 그 비율을 transform-origin 으로 그대로 넘긴다. 확대 배율은 ZOOM_FOCAL_STEP_RATIO 의 거듭제곱으로 계산해 휠을 반복할수록 배율이 점점 커지되 상한을 넘지 않게 막는다. 02 항목은 누적면적 57.5%·강도 평균 93.4·23프레임 중 23프레임에서 움직였다.

포인터 좌표를 기준으로 확대 중심을 계산하는 초점 줌
새 창에서 열기

02 · 포인터 좌표를 기준으로 확대 중심을 계산하는 초점 줌 — 측정 누적면적 57.451% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ZOOM_FOCAL_STEP_RATIOapplyWheeleaseInOut
  function applyWheel(dir, ox, oy) {
    manual.s = Math.max(1, Math.min(L2 * 1.4, manual.s * Math.pow(ZOOM_FOCAL_STEP_RATIO, dir * 0.6)));
    manual.x = ox; manual.y = oy;
    render(manual.s, manual.x, manual.y);
  }

  var userActive = false;
  function markActive() { if (!userActive) { userActive = true; root.classList.remove('is-demo'); manual.s = 1; render(1, .5, .5); } }

  view.addEventListener('wheel', function (e) {
    e.preventDefault();
    markActive();
    var r = view.getBoundingClientRect();
    var ox = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width));
    var oy = Math.max(0, Math.min(1, (e.clientY - r.top) / r.height));
    applyWheel(e.deltaY < 0 ? 1 : -1, ox, oy);
  }, { passive: false });

03일치한 검색어 위치를 계산해 점프하는 검색 하이라이트

입력한 글자로 시작하는 낱말을 문서 전체에서 찾아 목록으로 모으고, 하나를 고르면 그 요소의 세로 위치를 계산해 스크롤 애니메이션 목표로 삼는다. 이동이 끝나는 지점은 요소 위쪽에서 뷰포트 높이의 35%만큼 여유를 두어 하이라이트된 글자가 화면 가운데 쪽에 오게 만든다. 검색 대상은 미리 심어 둔 낱말 목록(alpha·theta)이라 03 항목은 누적면적 40.3%·강도 평균 67.8·23프레임 중 22프레임에서 움직였다.

일치한 검색어 위치를 계산해 점프하는 검색 하이라이트
새 창에서 열기

03 · 일치한 검색어 위치를 계산해 점프하는 검색 하이라이트 — 측정 누적면적 40.346% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

buildPagespageOfjumpTo
  function find(q) {
    q = q.toLowerCase();
    if (!q) return [];
    return words.filter(function (w) { return w.textContent.toLowerCase().indexOf(q) === 0; });
  }

  var scrollAnim = null;
  function startScroll(to, dur, nowMs) {
    scrollAnim = { from: doc.scrollTop, to: to, start: nowMs, dur: dur };
  }
  function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }

  var lastHit = null;
  function jumpTo(el, nowMs) {
    var target = Math.max(0, el.offsetTop - doc.clientHeight * 0.35);
    startScroll(target, 460, nowMs);
    if (lastHit) lastHit.classList.remove('pvs__w--hit');
    lastHit = el;
  }
  function ignite(el) { el.classList.add('pvs__w--hit'); }

04손을 뗄 때의 속도로 다음 장 여부를 계산하는 스와이프 플립

최근 120ms 안의 포인터 이동 샘플만 남겨두고 그 구간의 이동 거리를 경과 시간으로 나눠 px/s 단위 속도를 계산한다. 이 값이 SWIPE_FLIP_VELOCITY_PX_S 기준을 넘으면 카드가 그 방향으로 날아가고, 못 넘으면 원래 자리로 스프링처럼 되돌아간다. 04 항목은 누적면적 42.2%·강도 평균 27.2·23프레임 중 10프레임에서 움직였다(스와이프 사이 정지 구간이 있어 나머지는 정지 프레임이다).

손을 뗄 때의 속도로 다음 장 여부를 계산하는 스와이프 플립
새 창에서 열기

04 · 손을 뗄 때의 속도로 다음 장 여부를 계산하는 스와이프 플립 — 측정 누적면적 42.158% · 움직인 프레임 10/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SWIPE_FLIP_VELOCITY_PX_SvelocityNowspringEase
  function startDrag(x, t) { dragging = true; flying = false; springAnim = null; samples = []; dragStartX = x; cardStartX = curX; pushSample(x, t); }
  function moveDrag(x, t) {
    if (!dragging) return;
    var dx = x - dragStartX;
    curX = cardStartX + dx;
    curRot = dx / 22;
    pushSample(x, t);
    render();
  }
  function release(t) {
    if (!dragging) return;
    dragging = false;
    var v = velocityNow(t);
    if (Math.abs(v) >= SWIPE_FLIP_VELOCITY_PX_S) flyOut(v < 0 ? -1 : 1, t);
    else springBack(t);
  }

05받은 바이트 수로 진행률을 계산하는 프로그레시브 로드 바

세 개의 사인 함수를 더해 "빨라졌다 느려졌다"하는 다운로드 속도를 결정적으로 만들고, 그 속도를 매 프레임 누적해 받은 바이트 수를 늘린다. 진행률은 받은 바이트를 전체 크기로 나눈 값이고, 남은 페이지 수는 그 반대 비율에 전체 페이지 수를 곱해 표시한다. 진행 표시가 중심인 항목이라 05 항목은 누적면적 2.8%·강도 평균 114.3·23프레임 중 22프레임에서 움직였다 — 막대와 숫자만 바뀌고 화면 대부분은 그대로다.

받은 바이트 수로 진행률을 계산하는 프로그레시브 로드 바
새 창에서 열기

05 · 받은 바이트 수로 진행률을 계산하는 프로그레시브 로드 바 — 측정 누적면적 2.847% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

speedCurveTOTAL_BYTESHOLD_MS
  function speedCurve(tSec) {
    // 결정적 의사 지터: 세 개의 사인을 더해 "빨라졌다 느려졌다" 하는 네트워크 느낌
    var base = 210000;
    var v = base
      + 90000 * Math.sin(tSec * 2.3)
      + 40000 * Math.sin(tSec * 5.1 + 1.4)
      + 20000 * Math.sin(tSec * 9.7 + 0.6);
    return Math.max(30000, v);
  }

  var received = 0, speedMul = 1, paused = false, holdT = 0;

  function render() {
    var ratio = Math.max(0, Math.min(1, received / TOTAL_BYTES));
    fill.style.width = (ratio * 100).toFixed(1) + '%';
    pctEl.textContent = Math.round(ratio * 100) + '%';
    bytesEl.textContent = Math.round(received / 1000) + ' / ' + Math.round(TOTAL_BYTES / 1000) + ' KB';
    var remain = Math.ceil((1 - ratio) * TOTAL_PAGES);
    pagesEl.textContent = ratio >= 1 ? '전체 페이지 준비 완료' : ('남은 페이지 ' + remain);
  }

0690도 단위로 각도를 맞추는 회전 스냅 컨트롤

버튼을 누를 때마다 목표 각도가 ROTATE_SNAP_DEG 만큼 커지고, 현재 각도는 목표까지 남은 차이의 18%씩 매 프레임 좁혀가며 계속 다시 계산된다. 각도가 바뀌는 매 프레임마다 그 각도 기준 가로세로 박스 크기도 다시 계산해, 회전이 끝나야 딱 맞춰지는 스냅이 아니라 지나가는 모든 각도에서 비율이 맞다. 06 항목은 누적면적 11.4%·강도 평균 96.2·23프레임 중 23프레임에서 움직였다.

90도 단위로 각도를 맞추는 회전 스냅 컨트롤
새 창에서 열기

06 · 90도 단위로 각도를 맞추는 회전 스냅 컨트롤 — 측정 누적면적 11.449% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ROTATE_SNAP_DEGboxSizerotateBy
  function boxSize(angleDeg, w, h) {
    var r = angleDeg * Math.PI / 180;
    var c = Math.abs(Math.cos(r)), s = Math.abs(Math.sin(r));
    return { w: w * c + h * s, h: w * s + h * c };
  }

  var target = 0, current = 0;
  function render() {
    var box = boxSize(current, PAGE_W, PAGE_H);
    frame.style.width = box.w + 'px';
    frame.style.height = box.h + 'px';
    page.style.width = PAGE_W + 'px';
    page.style.height = PAGE_H + 'px';
    page.style.transform = 'translate(-50%,-50%) rotate(' + current.toFixed(1) + 'deg)';
    degEl.textContent = Math.round(((current % 360) + 360) % 360) + '°';
  }

  function rotateBy(dir) { target += dir * ROTATE_SNAP_DEG; }

07남은 스크롤 거리를 계산해 부드럽게 이동하는 키보드 페이지 이동

방향키나 PageUp·PageDown 을 누르면 다음 또는 이전 페이지 상단까지 남은 거리를 계산해 스크롤 애니메이션 목표로 등록하고, 그 거리를 520ms 동안 감속 곡선으로 나눠 이동한다. 목표 인덱스가 배열 범위를 벗어나지 않게 매번 다시 계산해 첫 페이지에서 위로, 마지막 페이지에서 아래로 눌러도 튕기지 않는다. 07 항목은 누적면적 67.6%·강도 평균 45.8·23프레임 중 23프레임에서 움직였다.

남은 스크롤 거리를 계산해 부드럽게 이동하는 키보드 페이지 이동
새 창에서 열기

07 · 남은 스크롤 거리를 계산해 부드럽게 이동하는 키보드 페이지 이동 — 측정 누적면적 67.574% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

easeScrollTonavigatepageTops
  var currentIndex = 0, scrollAnim = null;
  function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }
  function easeScrollTo(target, dur, nowT) { scrollAnim = { from: doc.scrollTop, to: target, start: nowT, dur: dur }; }
  function navigate(dir, nowT) {
    var idx = Math.max(0, Math.min(N - 1, currentIndex + dir));
    if (idx === currentIndex) return;
    currentIndex = idx;
    easeScrollTo(pageTops[idx], 520, nowT);
    badge.textContent = (idx + 1) + ' / ' + N;
  }

  var userActive = false;
  function markActive() { if (!userActive) { userActive = true; root.classList.remove('is-demo'); } }
  root.addEventListener('keydown', function (e) {
    if (e.key === 'ArrowDown' || e.key === 'PageDown') { e.preventDefault(); markActive(); navigate(1, lastTs || 0); }
    else if (e.key === 'ArrowUp' || e.key === 'PageUp') { e.preventDefault(); markActive(); navigate(-1, lastTs || 0); }
  });
  doc.addEventListener('wheel', function () { markActive(); }, { passive: true });

08받은 바이트 비율만큼 채워지는 다운로드 진행 링

다운로드가 시작되면 지금까지 받은 바이트 수를 전체 크기로 나눠 원형 테두리 둘레(CIRC) 중 채울 길이를 계산하고, 그 값을 stroke-dashoffset 에 넘겨 링이 그 비율만큼 차오르게 만든다. 100%에 닿으면 단계가 done 으로 바뀌면서 미리 길이를 계산해 둔 체크 표시 선이 그려지는 애니메이션으로 넘어간다. 08 항목은 누적면적 2.7%·강도 평균 132.2·23프레임 중 22프레임에서 움직였다 — 링과 버튼 문구만 바뀌는 항목이라 면적은 작다.

받은 바이트 비율만큼 채워지는 다운로드 진행 링
새 창에서 열기

08 · 받은 바이트 비율만큼 채워지는 다운로드 진행 링 — 측정 누적면적 2.737% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

lineLenCIRCfinish
  function startDownload(nowT) {
    if (phase === 'loading') return;
    phase = 'loading'; received = 0;
    root.classList.remove('pvg--done');
    checkA.style.strokeDashoffset = lenA.toFixed(2);
    checkB.style.strokeDashoffset = lenB.toFixed(2);
    btn.textContent = '받는 중';
    render(0);
  }
  function finish(nowT) {
    phase = 'done'; doneAt = nowT;
    root.classList.add('pvg--done');
    btn.textContent = '완료';
    checkAnim = { start: nowT };
  }
  function resetIdle() {
    phase = 'idle'; received = 0;
    root.classList.remove('pvg--done');
    btn.textContent = '다운로드';
    render(0);
  }

09여러 계산을 한 툴바에 모은 문서 뷰어 히어로 장면

01 의 스크롤 비율 계산, 02 의 포인터 초점 줌, 08 의 바이트 진행 링을 한 툴바 위에 동시에 올려 문서를 훑고 확대하고 내려받는 장면을 완성한다. 스크롤이 바뀌면 가장 많이 보이는 페이지를 다시 찾아 썸네일을 강조하고, 그 계산은 01 과 같은 겹침 비율 방식을 그대로 쓴다. 09 항목은 누적면적 40.1%·강도 평균 46.2·23프레임 중 21프레임에서 움직였다.

여러 계산을 한 툴바에 모은 문서 뷰어 히어로 장면
새 창에서 열기

09 · 여러 계산을 한 툴바에 모은 문서 뷰어 히어로 장면 — 측정 누적면적 40.053% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

HERO_ZOOM_STEPHERO_RAIL_LERPHERO_TOTAL_BYTES
  function computeCurrent() {
    var top = doc.scrollTop, h = doc.clientHeight, best = 0, bestRatio = -1;
    pages.forEach(function (pg, i) {
      var pTop = pg.offsetTop, pBot = pTop + pg.offsetHeight;
      var visTop = Math.max(top, pTop), visBot = Math.min(top + h, pBot);
      var ratio = Math.max(0, visBot - visTop) / pg.offsetHeight;
      if (ratio > bestRatio) { bestRatio = ratio; best = i; }
    });
    return best;
  }
  var activeIdx = -1;
  function markRail(idx) {
    if (idx === activeIdx) return;
    activeIdx = idx;
    thumbs.forEach(function (t, i) { t.classList.toggle('pht__thumb--active', i === idx); });
  }
  var railX = 0;
  function railTarget(idx) {
    var t = thumbs[idx]; if (!t) return 0;
    var want = t.offsetLeft + t.offsetWidth / 2 - rail.clientWidth / 2;
    return -Math.max(0, Math.min(railTrack.scrollWidth - rail.clientWidth, want));
  }

값이 반응을 바꾸는 곳 — 줌 초점 배율·스와이프 플립 속도·회전 스냅 각도

pdf 미리보기 프로그램마다 스크롤·확대·회전 감도가 다르게 느껴지는 이유는 화면에 보이는 요소 개수가 아니라 이 표의 세 수치 차이다.

항목 누적면적 % 강도 평균 움직인 프레임
01 썸네일 레일 동기화 59.6% 39.5 23/23
02 포인터 초점 줌 57.5% 93.4 23/23
03 검색 하이라이트 점프 40.3% 67.8 22/23
04 스와이프 플립 42.2% 27.2 10/23
05 프로그레시브 로드 바 2.8% 114.3 22/23
06 회전 스냅 컨트롤 11.4% 96.2 23/23
07 키보드 페이지 이동 67.6% 45.8 23/23
08 다운로드 진행 링 2.7% 132.2 22/23
09 툴바 히어로 장면 40.1% 46.2 21/23

ZOOM_FOCAL_STEP_RATIO 는 휠 한 번에 확대 배율이 몇 배씩 뛰는지를, SWIPE_FLIP_VELOCITY_PX_S 는 손을 얼마나 빨리 밀어야 다음 장으로 넘어가는지를, ROTATE_SNAP_DEG 는 버튼 한 번에 몇 도씩 회전하는지를 정한다. 아래 변형 표에서 이 세 상수를 직접 바꿔 감이 달라지는 걸 확인할 수 있다.

어디서 깨지는가 — 터치 스크롤 충돌·큰 파일 로딩 지연·키보드 대안 경로

01·07·09 처럼 스크롤 이벤트로 현재 페이지를 계산하는 항목은 모바일 브라우저의 관성 스크롤과 겹치면 scroll 이벤트가 한 프레임에 여러 번 몰려 들어와 계산이 튀는 순간이 있다 — 실무에서는 requestAnimationFrame 으로 계산 시점을 한 프레임에 한 번으로 묶어야 한다. 05·08 처럼 바이트 진행률을 계산하는 항목은 이 데모에서는 결정적 곡선으로 시뮬레이션했지만, 실제로 큰 파일을 받을 때는 속도가 순간적으로 0에 가까워지는 구간이 있어 막대가 한동안 멈춘 것처럼 보일 수 있다 — pdf 미리보기 오류로 오해받는 지점이 대개 여기다. 04 는 손을 뗄 때의 속도만 보고 방향을 정하는데, 짧고 빠르게 두 번 미는 동작에서는 첫 번째 스와이프가 끝나기 전에 두 번째 입력이 들어와 속도 계산이 뒤섞일 수 있다.

이 뷰어를 실무에 붙일 때는 이런 스크롤·네트워크 경합까지 같이 신경 써야 틈이 줄어든다. 아홉 개 원본이 들어 있는 압축 파일을 풀 때 묻는 비밀번호는 hat2hhry 이다.

변형 세 가지와 접근성

항목 상수 값 느낌
02 ZOOM_FOCAL_STEP_RATIO 1.2 → 2.4 값을 줄이면 휠 한 번에 조금씩만 확대되고, 값을 늘리면 같은 휠 동작에도 훨씬 크게 뛰어 확대된다
04 SWIPE_FLIP_VELOCITY_PX_S 300 → 600 값을 줄이면 살짝만 밀어도 다음 페이지로 넘어가고, 값을 늘리면 꽤 빠르게 밀어야만 넘어간다
06 ROTATE_SNAP_DEG 90 → 45 값을 늘리면 버튼 한 번에 큰 각도로 회전하고, 값을 줄이면 같은 버튼으로 더 잘게 나눠 회전한다

아래 설명은 각 항목 소스 안에 이미 들어 있는 분기 코드를 그대로 옮긴 것뿐, 별도로 덧붙인 처리는 없다. prefers-reduced-motion: reduce 분기는 matchMedia 로 아홉 항목 전부에 있어, 동작 줄임 설정을 켠 방문자에게는 자동 시연 없이 목표 상태(마지막 페이지 근처 스크롤·기본 배율·완료된 진행 바)로 바로 건너뛴다. 키보드 경로도 아홉 항목 전부에 keydown 리스너가 있다 — 01 은 방향키와 PageUp·PageDown, 07 도 같은 방향키·페이지 키로 페이지를 넘기고, 08 은 Enter·Space 로 다운로드를 시작하고 Escape 로 취소한다. 02 의 확대·06 의 회전도 각각 +·-, [·] 키로 같은 결과를 얻을 수 있다.

pdf 뷰어 ui는 라이브러리 없이 스크롤과 확대를 함께 계산할 수 있는가?

가능하다 — 이 아홉 항목 전부가 requestAnimationFrame 루프 하나와 좌표·비율 계산만으로 스크롤·확대·회전·진행률을 서로 맞춰 돌린다. 문서 협업 툴이나 계약서 열람 화면처럼 여러 기능을 한 화면에 붙여야 한다면 09 의 툴바 구성처럼 01(탐색)·02(확인)·08(내려받기) 세 계산만 겹쳐도 뷰어 하나의 완성 장면이 된다. 위젯 형태로 문서를 보여줘야 하는 자리라면 블로그 위젯 디자인 9종 — 공유·후기·지도 임베드 카드 의 카드 레이아웃과 같이 붙이면 되고, 확대 동작만 따로 떼어 다른 이미지에도 쓰고 싶다면 이미지 확대 보기 UI 9종 — 사진 파일 없이 만드는 뷰어 쪽 초점 계산과 비교해 보는 편이 낫다. 재생 진행 바처럼 시간축을 다루는 화면이라면 동영상 플레이어 만들기 9종 — 영상 파일 없이도 진짜로 돈다 의 진행률 계산과도 구조가 닮았다.

pdf 미리보기 구현에서 자주 나오는 질문

pdf 미리보기 스크롤이 자연스럽지 않고 뚝뚝 끊기면 뭘 의심해야 하나요?

01·07·09 는 스크롤·이동 값을 매 프레임 목표치의 일정 비율만큼만 좁혀가며 계산하는데, 이 비율 계산을 건너뛰고 값을 한 번에 바꾸면 중간 단계 없이 뚝 끊긴 것처럼 보인다. requestAnimationFrame 루프 밖에서 값을 직접 대입하는 코드가 있는지부터 확인하는 편이 낫다.

다른 문서 뷰어 프로그램에도 이 아홉 가지를 그대로 적용할 수 있나요?

여기서 계산하는 건 스크롤 비율·포인터 좌표·속도·바이트 수처럼 어느 문서 뷰어에도 있는 값이라, 실제 문서를 그리는 방식과 무관하게 페이지 계산·확대·진행률 표시 로직만 떼어 붙일 수 있다.

이 아홉 개를 하나의 뷰어에 전부 같이 써도 되나요?

다 붙이는 것보다는 09 정도로 두세 항목만 골라 겹치는 편이 화면 계산량 면에서 안전하다. 스크롤 동기화·초점 줌·회전 재계산이 각자 프레임마다 좌표를 다시 구하는 구조라, 한 화면에 다 올리면 그 계산이 겹겹이 쌓인다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.