01무한 스크롤 사진 벽
작은 정사각 사진들이 가로로 끊임없이 흘러가고, 줄을 통째로 복제해 이어 붙여 놓아서 한 바퀴 돌아 처음으로 돌아가는 지점이 전혀 티가 나지 않습니다.
쓰임인스타그램 피드를 모아 보여주는 랜딩 배경, 로고 대신 작업 사진을 흘리는 소개 섹션
translateX(-50%) 무한 루프track 통째 복제margin-right 간격

<main class="stage">
<div class="wall-viewport" aria-label="사진이 끝없이 흐르는 갤러리">
<ul class="wall-track is-demo">
<li class="wall-tile wall-tile--pink"></li>
<li class="wall-tile wall-tile--mint"></li>
<li class="wall-tile wall-tile--lilac"></li>
<li class="wall-tile wall-tile--sky"></li>
<li class="wall-tile wall-tile--pink2"></li>
<li class="wall-tile wall-tile--pink" aria-hidden="true"></li>
<li class="wall-tile wall-tile--mint" aria-hidden="true"></li>
<li class="wall-tile wall-tile--lilac" aria-hidden="true"></li>
<li class="wall-tile wall-tile--sky" aria-hidden="true"></li>
<li class="wall-tile wall-tile--pink2" aria-hidden="true"></li>
</ul>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 한 바퀴 도는 시간
$easing: linear;
$color: $subject-pink; // 대표 타일 색
@include stage($stage-paper);
.wall-viewport {
width: min(280px, 84vw);
overflow: hidden;
border-radius: $r-card;
box-shadow: $shadow-press;
}
.wall-track {
display: flex;
list-style: none;
margin: 0;
padding: 0;
width: max-content;
}
// 간격은 margin-right 로만 준다 (BD15) — flex gap 을 쓰면 -50% 가 gap/2 만큼 모자라 튄다
.wall-tile {
flex: 0 0 auto;
width: 56px;
height: 42px;
margin-right: $sp-2;
border-radius: $r-sm;
}
.wall-tile--pink { background: linear-gradient(155deg, $color 0%, $subject-cream 100%); }
.wall-tile--mint { background: linear-gradient(155deg, $subject-mint 0%, $subject-cream 100%); }
.wall-tile--lilac { background: linear-gradient(155deg, $subject-lilac 0%, $subject-cream 100%); }
.wall-tile--sky { background: linear-gradient(155deg, $subject-sky 0%, $subject-cream 100%); }
.wall-tile--pink2 { background: linear-gradient(205deg, $subject-cream 0%, $color 100%); }
// 트랙을 통째로 복제(위 10칸 = 5칸 × 2벌)해 두면 -50% 지점이 처음과 픽셀 단위로 같아 이어붙인 자리가 안 보인다
.wall-track.is-demo { animation: wall-loop $duration $easing infinite; }
@keyframes wall-loop {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
.wall-track.is-demo { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.wall-viewport {
width: min(280px, 84vw);
overflow: hidden;
border-radius: 16px;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
}
.wall-track {
display: flex;
list-style: none;
margin: 0;
padding: 0;
width: max-content;
}
.wall-tile {
flex: 0 0 auto;
width: 56px;
height: 42px;
margin-right: 8px;
border-radius: 8px;
}
.wall-tile--pink {
background: linear-gradient(155deg, #ff9ebb 0%, #fff7e6 100%);
}
.wall-tile--mint {
background: linear-gradient(155deg, #4cd4a6 0%, #fff7e6 100%);
}
.wall-tile--lilac {
background: linear-gradient(155deg, #b9a0ff 0%, #fff7e6 100%);
}
.wall-tile--sky {
background: linear-gradient(155deg, #6ec8ff 0%, #fff7e6 100%);
}
.wall-tile--pink2 {
background: linear-gradient(205deg, #fff7e6 0%, #ff9ebb 100%);
}
.wall-track.is-demo {
animation: wall-loop 2s linear infinite;
}
@keyframes wall-loop {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
@media (prefers-reduced-motion: reduce) {
.wall-track.is-demo {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./InfiniteScrollPhotoWall.module.scss";
export type InfiniteScrollPhotoWallProps = {
/** 한 바퀴 도는 시간 (기본 2000ms) */
duration?: number;
easing?: string;
/** 대표 타일 색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const ORDER = ["a", "b", "c", "d", "e", "a", "b", "c", "d", "e"] as const;
/** 작은 사진들이 가로로 끝없이 흐르고, 트랙을 통째로 복제해 이어붙인 자리가 안 보인다 */
export default function InfiniteScrollPhotoWall({
duration = 2000,
easing = "linear",
color = "#ff9ebb",
}: InfiniteScrollPhotoWallProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.viewport} style={vars} aria-label="A gallery of photos scrolling endlessly">
<ul className={styles.track}>
{ORDER.map((tone, i) => (
<li key={i} className={`${styles.tile} ${styles["tile-" + tone]}`} aria-hidden={i >= 5} />
))}
</ul>
</div>
);
}01 무한 스크롤 사진 벽 — 측정 누적면적 8.037% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 포트폴리오 갤러리 UI 9종 — 드래그 정렬부터 모프 전환까지
이 데모가 속한 글
포트폴리오 갤러리 UI 9종 — 드래그 정렬부터 모프 전환까지포트폴리오 갤러리는 사진이나 작업물을 격자로 늘어놓고, 마우스를 올리거나 클릭했을 때 확대·정렬·전환이 일어나는 화면입니다. 핀터레스트에 저장해 둔 그 포트폴리오 갤러리를 아홉 가지 방식으로 나눠 그대로 코드로 드립 이 글의 데모 전부 보기


