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

종이 넘기는 애니메이션 9가지 — 캔버스 페이지 컬

종이 넘기는 애니메이션은 캔버스 계산만으로 종이를 접고 마는 아홉 가지 패턴이다. 이 페이지 컬은 라이브러리 없이도 앞으로 계속 이렇게 움직일 것이다 — 01은 포인터가 닿은 자리부터 말리고, 07은 흔들림 값만큼 비행기가 기울며, 09는 그 둘을 한 책상 장면으로 합쳐 보여줄 것이다.

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

종이라는 재질감은 어디서 오나 — 아홉 계산이 갈리는 지점

아홉 장면 모두 <canvas> 위에 매 프레임 fillRect·createLinearGradient만 다시 그린다. 종이처럼 보이는 이유는 이미지가 아니라 각도(theta)의 사인·코사인, 노이즈 격자, 관절 사슬 같은 값 하나가 명도와 위치를 계산해서다. 아래 아홉 개는 그 값이 어느 자리에 꽂히는지를 하나씩 보여준다.

01포인터로 당기면 모서리부터 말리는 페이지 컬

모서리를 당긴 거리(dx)를 CURL_RADIUS_PX(60)로 나눈 값이 회전각(theta)이 되고, 그 각의 사인·코사인으로 원기둥에 감듯 앞뒤 면의 위치·밝기를 계산한다. 짧게 당기면 살짝만 말리고, 거리가 반경을 넘어서면 뒷면 색이 드러날 만큼 완전히 뒤집힌다.

포인터로 당기면 모서리부터 말리는 페이지 컬
새 창에서 열기

01 · 포인터로 당기면 모서리부터 말리는 페이지 컬 — 측정 누적면적 35.251% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CURL_RADIUS_PXfoldThetacurlPage
    for (var x = foldX; x <= PAGE_R + STRIP_W; x += STRIP_W) {
      var dx = Math.min(x - foldX, maxDx);
      var theta = foldTheta(dx);
      if (theta > Math.PI + 0.15) continue;
      var sxp = foldX + Math.sin(Math.min(theta, Math.PI)) * CURL_RADIUS_PX;
      var front = Math.cos(theta) > 0;
      var lift = (dx / maxDx) * skew * 30;
      ctx.fillStyle = front ? shadeHex(PAPER, 0.86 + 0.14 * Math.cos(theta)) : shadeHex(PAPER_BACK, 0.5 + 0.3 * (-Math.cos(theta)));
      ctx.fillRect(sxp, PAGE_T + lift, STRIP_W + 1, h);
    }

01 항목은 누적면적 35.3%·강도 평균 90.2·23프레임 중 23프레임에서 움직였다.

02접힘선을 따라 그림자가 계산되는 페이지 플립

접힘선(FOLD_X)에서 각도(angle)가 0부터 π까지 도는 동안 ctx.scale(Math.cos(angle), 1)로 페이지 폭을 눌러 두께를 표현하고, 같은 각의 사인 값만큼 그림자 폭과 진하기를 함께 키운다. 각도가 π/2를 지나면 뒷면 색이 채워져 방향이 뒤집힌다.

접힘선을 따라 그림자가 계산되는 페이지 플립
새 창에서 열기

02 · 접힘선을 따라 그림자가 계산되는 페이지 플립 — 측정 누적면적 41.608% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

FOLD_XflipAnglepageFlip
    var c = Math.cos(angle);
    var frontFacing = c >= 0;
    var scaleW = Math.max(0.001, Math.abs(c)) * w;
    ctx.save();
    ctx.translate(FOLD_X, 0);
    ctx.scale(c === 0 ? 0.001 : c, 1);
    ctx.fillStyle = frontFacing ? FRONT_PAGE : FRONT_PAGE_BACK;
    ctx.fillRect(0, BOOK_T, w, h);
    if (frontFacing) drawLines(6, BOOK_T, w - 12, h, LINE_COLOR);
    ctx.restore();

    var shadowW = 6 + Math.abs(Math.sin(angle)) * 46;
    var shadowA = 0.08 + Math.abs(Math.sin(angle)) * 0.34;
    var gx = FOLD_X + scaleW * (frontFacing ? 1 : -1);
    var dir = frontFacing ? -1 : 1;
    var g = ctx.createLinearGradient(gx, 0, gx + shadowW * dir, 0);
    g.addColorStop(0, 'rgba(0,0,0,' + shadowA + ')');
    g.addColorStop(1, 'rgba(0,0,0,0)');
    ctx.fillStyle = g;
    ctx.fillRect(Math.min(gx, gx + shadowW * dir), BOOK_T, shadowW, h);
  }

02 항목은 누적면적 41.6%·강도 평균 73.4·23프레임 중 20프레임에서 움직였다.

03여러 장이 차례로 넘어가는 책 스택 플립

남은 장 수(remaining)에서 지금 넘어가는 한 장을 뺀 값을 stackThickness가 2.2배로 늘려, 쌓인 종이 두께만큼 그림자 사각형을 오른쪽 아래로 밀어낸다. 다섯 장(STACK_TOTAL)을 다 넘기면 remaining이 다시 처음 값으로 돌아가 순환한다.

여러 장이 차례로 넘어가는 책 스택 플립
새 창에서 열기

03 · 여러 장이 차례로 넘어가는 책 스택 플립 — 측정 누적면적 45.378% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

STACK_TOTALstackThicknessnextPage
  function drawStackShadow() {
    var th = stackThickness(remaining - (flipping ? 1 : 0));
    ctx.fillStyle = 'rgba(23,20,26,.5)';
    ctx.fillRect(BOOK_L + th, BOOK_T + th, BOOK_R - BOOK_L, BOOK_B - BOOK_T);
  }

  function drawLines(x, y, w, h) {
    ctx.strokeStyle = 'rgba(23,20,26,.24)';
    ctx.lineWidth = 1.4;
    for (var i = 0; i < 4; i += 1) {
      var ly = y + h * (0.2 + i * 0.18);
      ctx.beginPath(); ctx.moveTo(x + w * 0.1, ly); ctx.lineTo(x + w * 0.9, ly); ctx.stroke();
    }
  }

03 항목은 누적면적 45.4%·강도 평균 75.7·23프레임 중 18프레임에서 움직였다.

04책 중심 골 곡률로 어두워지는 펼침 장면

책 중심(CENTER)에서 바깥 가장자리로 갈수록 줄어드는 비율(1-t)에 곡률 값(bendAngle)을 곱해 각 줄무늬가 눌리는 위치(comp)와 명도(shade)를 함께 정한다. 각도가 BEND_MAX_RAD(26도)에 가까워질수록 골 쪽 면이 더 짙게 어두워진다.

책 중심 골 곡률로 어두워지는 펼침 장면
새 창에서 열기

04 · 책 중심 골 곡률로 어두워지는 펼침 장면 — 측정 누적면적 31.942% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

BEND_MAX_RADgutterBenddrawPageSide
  function drawPageSide(sign) {
    var strip = 4, h = PAGE_B - PAGE_T;
    for (var dx = 0; dx <= HALF_W; dx += strip) {
      var t = dx / HALF_W;                             // 0=골 · 1=바깥 가장자리
      var curveTheta = (1 - t) * bendAngle;             // 골에 가까울수록 곡률이 크다
      var comp = Math.sin(curveTheta) * 12;             // 골 쪽으로 눌리는 폭(px)
      var x = CENTER + sign * (dx - comp);
      var shade = 1 - (1 - t) * (bendAngle / BEND_MAX_RAD) * 0.42;
      ctx.fillStyle = shadeHex(PAPER, shade);
      ctx.fillRect(sign > 0 ? x : x - strip, PAGE_T, strip + 1, h);
    }
  }

04 항목은 누적면적 31.9%·강도 평균 22.8·23프레임 중 23프레임에서 움직였다.

05접선 각도로 꺾이는 오리가미 폴드

접힘 각도(angleDeg)를 라디안으로 바꾼 코사인 값 하나로 두 면의 폭(ctx.scale)과 밝기(brightness)를 동시에 계산해, 각도가 5도에 가까우면 거의 평평하고 150도에 가까우면 뒷면이 정면으로 접혀 보인다. 접힘선 가운데 세로줄은 앞면일 때만 그려 넣어 구김 자국을 흉내 낸다.

접선 각도로 꺾이는 오리가미 폴드
새 창에서 열기

05 · 접선 각도로 꺾이는 오리가미 폴드 — 측정 누적면적 29.063% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CREASE_ANGLE_DEGangleStepdrawFacet
  function drawFacet(sign) {
    var rad = angleDeg * Math.PI / 180;
    var c = Math.cos(rad);
    var frontFacing = c >= 0;
    var h = BOTTOM - TOP;
    ctx.save();
    ctx.translate(CENTER, 0);
    ctx.scale(sign * (c === 0 ? 0.001 : c), 1);
    var brightness = frontFacing ? (0.62 + 0.38 * c) : (0.3 + 0.2 * (-c));
    ctx.fillStyle = frontFacing ? shadeHex(PAPER, brightness) : shadeHex(PAPER_BACK, brightness);
    ctx.fillRect(0, TOP, FACET_W, h);
    if (frontFacing) {
      ctx.strokeStyle = 'rgba(23,20,26,.18)';
      ctx.lineWidth = 1;
      ctx.beginPath(); ctx.moveTo(FACET_W * 0.5, TOP + 8); ctx.lineTo(FACET_W * 0.5, BOTTOM - 8); ctx.stroke();
    }
    ctx.restore();
  }

05 항목은 누적면적 29.1%·강도 평균 104.3·23프레임 중 23프레임에서 움직였다.

06구겨진 표면이 노이즈로 음영지는 종이 질감

24×24 격자에 심어 둔 난수(lattice)를 좌표 사이에서 smoothStep으로 부드럽게 보간하는 2D 노이즈 함수가 값을 만들고, 서로 다른 배율·위상으로 부른 노이즈 두 겹을 섞은 게 crumpleHeight다. 이 높낮이 값이 구겨진 종이 표면의 픽셀별 명도로 그대로 옮겨진다.

구겨진 표면이 노이즈로 음영지는 종이 질감
새 창에서 열기

06 · 구겨진 표면이 노이즈로 음영지는 종이 질감 — 측정 누적면적 50.638% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

makeValueNoisecrumpleHeightlatAt
    var rnd = seedRand(seed);
    var GRID = 24;
    var lattice = [];
    for (var i = 0; i < GRID * GRID; i += 1) lattice.push(rnd());
    function latAt(xi, yi) {
      xi = ((xi % GRID) + GRID) % GRID; yi = ((yi % GRID) + GRID) % GRID;
      return lattice[yi * GRID + xi];
    }
    return function valueNoise(x, y) {
      var xi = Math.floor(x), yi = Math.floor(y);
      var xf = x - xi, yf = y - yi;
      var v00 = latAt(xi, yi), v10 = latAt(xi + 1, yi), v01 = latAt(xi, yi + 1), v11 = latAt(xi + 1, yi + 1);
      var sx = smoothStep(xf), sy = smoothStep(yf);
      var a = v00 + (v10 - v00) * sx;
      var b = v01 + (v11 - v01) * sx;
      return a + (b - a) * sy;
    };
  }
  var valueNoise = makeValueNoise(41306);

  function crumpleHeight(x, y, phase) {
    var n1 = valueNoise(x * 0.16 + phase, y * 0.16);
    var n2 = valueNoise(x * 0.4 - phase * 1.4, y * 0.4 + phase * 0.6);
    return n1 * 0.66 + n2 * 0.34;
  }

06 항목은 누적면적 50.6%·강도 평균 20.7·23프레임 중 23프레임에서 움직였다.

07포물선과 흔들림 값으로 나는 종이비행기

이륙(LAUNCH)에서 착륙(LAND)까지 4·t·(1-t) 포물선으로 오르내리는 좌표에 사인파 흔들림(DRIFT_AMPLITUDE_PX)을 더해 위치를 구하고, 같은 포물선의 높이값을 반대로 그림자 반경·투명도에 반영해 비행기가 높이 뜰수록 그림자는 작고 흐려진다. 종이 비행기 애니메이션치고는 드물게 그림자까지 고도 값 하나로 따라온다.

포물선과 흔들림 값으로 나는 종이비행기
새 창에서 열기

07 · 포물선과 흔들림 값으로 나는 종이비행기 — 측정 누적면적 20.715% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

DRIFT_AMPLITUDE_PXdriftFlightdrawPlane
  function driftFlight(t) {
    var x = lerp(LAUNCH.x, LAND.x, t) + Math.sin(t * DRIFT_CYCLES * Math.PI * 2) * DRIFT_AMPLITUDE_PX;
    var y = lerp(LAUNCH.y, LAND.y, t) - ARC_HEIGHT * 4 * t * (1 - t);
    return { x: x, y: y };
  }

  var pos = driftFlight(0), heading = 0, curT = 0;
  var trail = [];
  var TRAIL_MAX = 18;

  function drawShadow(x, t) {
    var alt = ARC_HEIGHT * 4 * t * (1 - t);
    var rx = clamp(34 - alt * 0.28, 8, 34);
    var ry = rx * 0.36;
    var alpha = clamp(0.32 - alt * 0.003, 0.06, 0.32);
    ctx.fillStyle = 'rgba(10,8,12,' + alpha + ')';
    ctx.beginPath();
    ctx.ellipse(x, FLOOR_Y, rx, ry, 0, 0, Math.PI * 2);
    ctx.fill();
  }

07 항목은 누적면적 20.7%·강도 평균 188.1·23프레임 중 23프레임에서 움직였다.

08관절 값이 이어져 흔들리는 종이 인형

어깨부터 손끝까지 관절마다 FK_DELAY_RAD만큼 위상을 늦춘 사인파(sway)가 이전 관절의 누적 각도(cum)에 더해져, 팔 하나를 흔들면 다음 관절이 시차를 두고 뒤따르는 사슬(FK chain)이 만들어진다. 종이 인형 애니메이션답게 관절 감쇠(0.82를 관절 순서만큼 거듭제곱한 값)가 바깥 관절일수록 흔들림을 줄여 준다.

관절 값이 이어져 흔들리는 종이 인형
새 창에서 열기

08 · 관절 값이 이어져 흔들리는 종이 인형 — 측정 누적면적 17.033% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

FK_DELAY_RADjointAnglefkChain
  function jointAngle(t, def, chainIndex) {
    var w = Math.PI * 2 * t;
    var sway = Math.sin(w - chainIndex * FK_DELAY_RAD) * rad(SWING_AMP_DEG) * Math.pow(0.82, chainIndex);
    return def.base + sway;
  }

  function swayFactor(t) { return Math.sin(Math.PI * 2 * t); }

  function fkChain(originX, originY, segs, t) {
    var x = originX, y = originY, cum = 0;
    var pts = [{ x: x, y: y }];
    for (var i = 0; i < segs.length; i += 1) {
      cum += jointAngle(t, segs[i], i);
      x += Math.cos(cum) * segs[i].seg[0];
      y += Math.sin(cum) * segs[i].seg[0];
      pts.push({ x: x, y: y });
    }
    return pts;
  }

08 항목은 누적면적 17.0%·강도 평균 95.2·23프레임 중 23프레임에서 움직였다.

09위 장면을 한 책상 위에 모은 페이지 넘김 히어로

책상 장면은 compositeValue 값 하나를 책 골 곡률·컬 반경·비행기 흔들림 폭에 동시에 곱해, 값이 0에 가까우면 셋 다 얌전하고 1에 가까우면 함께 크게 움직인다. 01의 원기둥 투영과 04의 골 음영, 07의 표류가 한 좌표계에서 같은 값을 공유한다.

위 장면을 한 책상 위에 모은 페이지 넘김 히어로
새 창에서 열기

09 · 위 장면을 한 책상 위에 모은 페이지 넘김 히어로 — 측정 누적면적 17.499% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CURL_RADIUS_MAX_PXcompositeValuedrawBook
  function curlPage(t) {
    var radius = 6 + t * CURL_RADIUS_MAX_PX;
    var stripCount = 14;
    for (var i = 0; i < stripCount; i += 1) {
      var u = i / stripCount;
      var localX = u * CURL_W;
      var theta = Math.max(0, localX - (CURL_W - radius * 2.4)) / Math.max(1, radius);
      theta = clamp(theta, 0, Math.PI);
      var sx = CURL_CX + CURL_W - CURL_W * (1 - u) - Math.min(localX, CURL_W - radius * 2.4);
      var screenX = CURL_CX + (CURL_W - radius * 2.4) * u * 0 + localX - (1 - Math.cos(theta)) * radius;
      var front = Math.cos(theta) >= 0;
      var bright = front ? (0.6 + 0.4 * Math.cos(theta)) : (0.32 + 0.18 * (-Math.cos(theta)));
      ctx.fillStyle = front ? shadeHex(PAPER, bright) : shadeHex(PAPER_BACK, bright);
      var stripX = CURL_CX + localX - (localX > (CURL_W - radius * 2.4) ? (1 - Math.cos(theta)) * radius : 0);
      ctx.fillRect(stripX, CURL_TOP, CURL_W / stripCount + 1, CURL_BOT - CURL_TOP);
    }
  }

09 항목은 누적면적 17.5%·강도 평균 123.2·23프레임 중 23프레임에서 움직였다.

이 아홉 장면이 실제로 쓰이는 화면

01 모서리 컬은 전자책 리더 페이지 넘김에, 02 접힘선 플립은 갤러리 다음 이미지 버튼에, 03 스택 플립은 카탈로그 뷰어에, 04 골 음영은 레시피 펼침 화면에 붙는다.

05 오리가미 폴드는 소개 인트로에, 06 구김 질감은 메모 카드 배경에, 07 종이비행기는 빈 상태 일러스트에, 08 종이 인형은 로딩 마스코트에, 09 책상 장면은 랜딩 히어로에 값 하나로 쓰인다.

컬 반경·접힘 각도·흔들림 폭이 갈리는 자리

값이 꽂히는 자리는 셋이다. 01·09는 컬 반경이 커질수록 말리는 폭이 넓어지고, 04·05는 접힘 각도가 커질수록 깊어진다. 07은 흔들림 폭으로 출렁임을 바꾼다.

NN 이름 누적면적 % 강도 평균 x/23
01 모서리 컬 35.3 90.2 23/23
02 접힘선 플립 41.6 73.4 20/23
03 다중 페이지 스택 45.4 75.7 18/23
04 책 골 음영 31.9 22.8 23/23
05 오리가미 폴드 29.1 104.3 23/23
06 구김 질감 50.6 20.7 23/23
07 종이비행기 20.7 188.1 23/23
08 종이 인형 17.0 95.2 23/23
09 책상 히어로 17.5 123.2 23/23

동작 줄임 설정에서도 리스너가 남으면 깨지는 지점

prefers-reduced-motion: reduce가 참이면 아홉 항목 모두 is-demo 클래스를 지우고 자동 재생 루프를 끈다. 다만 pointerdown·keydown 리스너는 남아 01·05·09는 화살표 키(←→)로 여전히 움직인다 — "reduce면 전부 정지"로 오해하면 QA에서 깨지는 지점이다. 저사양 기기에서는 06이 가장 부담이 크다. 320×200 격자 안에서 매 프레임 valueNoise를 픽셀 단위로 호출해 명도를 다시 계산하는데, requestAnimationFrame 콜백 안에서 이 계산이 밀리면 프레임이 그대로 떨어진다.

값 세 가지만 바꿔보는 변형과 접근성

변형 바꾼 값 느낌
페이지 컬 반경 값 조정 01 CURL_RADIUS_PX 60 → 160 값을 줄이면 모서리가 아주 좁게 말려 살짝 접힌 듯 보이고, 값을 늘리면 훨씬 넓게 말려 원통에 가까운 굴림으로 보여야 한다
접힘 각도 값 조정 05 CREASE_ANGLE_DEG 30 → 120 값을 줄이면 살짝만 꺾여 거의 평평한 채로 남고, 값을 늘리면 깊게 꺾여 완전히 접힌 입체 형태로 보여야 한다
종이비행기 흔들림 값 조정 07 DRIFT_AMPLITUDE_PX 8 → 30 값을 줄이면 거의 직선에 가깝게 날아가고, 값을 늘리면 좌우로 크게 흔들리며 날아가야 한다

접근성은 두 층으로 나뉜다. 첫째, 아홉 항목 전부 window.matchMedia('(prefers-reduced-motion: reduce)').matches 값을 시작할 때 한 번 읽어 자동 재생 루프를 끈다. 둘째, 01·05·09는 포인터뿐 아니라 키보드 화살표(←→, 01·09는 ↑↓도)로도 같은 값을 조작할 수 있어 마우스 없이도 컬 반경·접힘 각도·책상 장면의 값을 직접 확인할 수 있다. seedRand로 고정한 시드값 덕분에 .is-demo 자동 시연은 매번 같은 경로로 움직이며, 실제 사용자 입력을 흉내 낸 것일 뿐 진짜 클릭을 대신하지는 않는다.

zip 비밀번호 3zppfspp로 폴더를 풀면 fit() 함수가 컨테이너 크기에 맞춰 캔버스를 다시 스케일하는 부분도 그대로 들어 있다 — 고정 폭(LW 320) 컨테이너 없이 반응형 폭에 바로 얹으면 레이아웃이 살짝 흔들릴 수 있다.

종이 접기 애니메이션과 이 페이지 컬 데모, 어디서 갈라지는가?

종이 접기 애니메이션이라는 이름으로 검색하면 대부분 transform: rotateY와 backface-visibility: hidden을 쓴 단일 힌지 접기가 나온다 — 면이 두 장뿐이라 CSS 트랜지션만으로 충분하다. 이 페이지 컬은 getContext('2d') 위에서 각도마다 픽셀 줄무늬를 따로 계산해 원기둥에 감기는 곡면·구겨진 표면의 노이즈 높낮이·관절이 이어지는 사슬까지 만든다는 점에서 다루는 계산 자체가 다르다. 카드가 뒤집히는 단순한 두 면 전환은 CSS 카드 뒤집기 애니메이션 9종에서, 스크롤에 맞춰 장면이 바뀌는 연출은 스크롤 스토리텔링 섹션 9종에서, 종이 대신 로프·래그돌처럼 관절이 물리적으로 반응하는 계산은 자바스크립트 물리엔진 원리 9종에서 각각 다룬다. 캔버스 2D API 자체의 좌표계·변환 문법은 MDN Canvas API 문서에 정리돼 있다.

종이 넘기는 애니메이션을 처음 붙일 때 자주 나오는 질문

CSS transform 만으로 페이지 컬을 만들 수 있나요?

단일 힌지 접기 정도는 rotateY와 backface-visibility로 충분하다. 다만 01처럼 모서리가 원기둥에 감기듯 곡면을 그리려면 각도마다 다른 폭·명도를 가진 줄무늬를 따로 그려야 하는데, 이건 CSS 단독 문법으로는 표현할 수 없고 requestAnimationFrame 루프 안에서 픽셀 단위로 계산해야 한다.

종이책 애니메이션처럼 여러 장을 순서대로 넘기려면 어떤 항목을 보면 되나요?

03 다중 페이지 스택이 그 방식이다. remaining 값이 줄어들 때마다 stackThickness가 남은 장 수만큼 그림자 두께를 다시 계산해, 종이책 애니메이션에서 기대하는 "쌓인 만큼 두꺼워 보이는" 인상을 값 하나로 유지한다.

포인터가 없는 터치 전용 기기에서도 컬이 동작하나요?

01의 calcFromEvent 함수는 e.touches && e.touches[0]을 먼저 확인해 터치 좌표를 우선 읽고, 없으면 마우스 이벤트 좌표로 넘어간다. pointerdown·pointermove는 Pointer Events API라 마우스·펜·터치를 같은 코드로 받으므로 별도 분기 없이도 동작한다.

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

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