[TypeScript/JavaScript] 직접 벽돌쌓기 형식의 리스트 레이아웃 만들기
반응형
조적조(Masonry) 형식의 레이아웃은 핀터레스트(Pinterest)형식으로 폭이 같지만 높이가 다른 여러 객체를 쌓아나가는 형태의 레이아웃입니다. Masonry라는 자바스크립트 라이브러리를 사용해서 레이아웃을 정렬하는 방법을 많이 사용합니다. 여기서는 라이브러리를 사용하지 않고 직접 스크립트를 작성합니다.
기본 원리
작업을 시작하기 전에 배치할 리스트의 컨테이너와 아이템의 전제를 살펴봅니다.
- 상대적인 위치를 가진 컨테이너에 배치할 아이템은 절대 좌표를 사용합니다.
- 각 아이템은 폰트와 이미지를 모두 로드한 뒤에는 높이가 변하지 않습니다.
- 아이템 배치는 컨테이너의 너비를 벗어나면 안됩니다.
- 뷰포트의 크기가 변하면 아이템의 너비가 바뀔 수 있습니다.
나중에 스타일을 스크립트로 추가할 수도 있지만 스타일시트를 먼저 작성합니다.
.list-container {
position: relative;
overflow: hidden;
transition: height 0.3s ease-in;
}
.list-container .list-item {
position: absolute;
width: calc(100% / 3);
box-sizing: border-box;
opacity: 0;
transition: opacity 0.8s ease-in;
}
.list-container .list-item.show {
opacity: 1;
}
레이아웃 아이템 사이의 간격을 조정하고 싶다면 배치할 실제 요소를 .list-item
로 감싸고 .list-item
에는 padding
속성을 추가합니다. 이제 배치 절차를 생각합니다.
// listContainerQuery: 컨테이너 선택자, listItemQuery: 아이템 선택자
function MasonryLayout(listContainerQuery: string, listItemQuery: string) {
// 1. 레이아웃 함수
// 1-1. 컨테이너에 몇 행의 아이템이 들어갈 수 있는지 계산
// 1-2. 각 행의 높이를 담아두는 배열 생성
// 1-3. 각 아이템 요소 루프
// 1-3-1. 이미지 로딩 대기
// 1-3-2. 아이템을 배치할 가장 작은 값의 행 찾기
// 1-3-3. 아이템의 배치 속성 변경(top, left)
// 1-3-4. 현재 행의 높이에 아이템의 높이 합산
// 1-3-5. 높이 행에서 가장 큰 값으로 컨테이너 높이를 변경
// 2. 레이아웃 함수를 뷰포트 사이즈 변경 이벤트에 실행
}
타입스크립트
컴파일을 거쳐야 하는 불편함이 있지만 개발할 때는 이쪽을 추천합니다.
See the Pen Untitled by SeungWan Jin (@soma0sd) on CodePen.
function MasonryLayout(listContainerQuery: string, listItemQuery: string) {
// 선택자로부터 요소를 얻음
const elemContainer = document.querySelector<HTMLElement>(listContainerQuery);
const elemFirstItem = elemContainer.querySelector<HTMLElement>(listItemQuery);
// 배열 중 가장 작은 값의 인덱스
const getArrayMinIndex = (arr: any[]) =>
arr.reduce((r, v, i, a) => (v >= a[r] ? r : i), -1);
// 배열 중 가장 큰 값의 인덱스
const getArrayMaxIndex = (arr: any[]) =>
arr.reduce((r, v, i, a) => (v <= a[r] ? r : i), -1);
// 아이템 내부의 이미지가 로드될 때까지 대기
const waitImageLoad = async (elem: HTMLElement) => {
for (const img of elem.querySelectorAll<HTMLImageElement>("img"))
await img.decode();
};
// 1. 레이아웃 함수
const LayoutSetup = async () => {
// 1-1. 컨테이너에 몇 행의 아이템이 들어갈 수 있는지 계산
const widthWrapper = elemContainer.offsetWidth;
const widthItem = elemFirstItem.offsetWidth;
// 1-2. 각 행의 높이를 담아두는 배열 생성
let layoutTopArr = new Array(parseInt(`${widthWrapper / widthItem}`));
layoutTopArr.fill(0);
// 1-3. 각 아이템 요소 루프
for (const elem of elemContainer.querySelectorAll<HTMLElement>(listItemQuery)) {
// 1-3-1. 이미지 로딩 대기
await waitImageLoad(elem);
// 1-3-2. 아이템을 배치할 가장 작은 값의 행 찾기
let topMinIndex = getArrayMinIndex(layoutTopArr);
// 1-3-3. 아이템의 배치 속성 변경(top, left)
elem.style.left = topMinIndex * widthItem + "px";
elem.style.top = layoutTopArr[topMinIndex] + "px";
// 1-3-4. 숨겼던 아이템을 표시
elem.classList.add("show");
// 1-3-5. 현재 행의 높이에 아이템의 높이 합산
layoutTopArr[topMinIndex] += elem.offsetHeight;
// 1-3-6. 높이 행에서 가장 큰 값으로 컨테이너 높이를 변경
elemContainer.style.height =
layoutTopArr[getArrayMaxIndex(layoutTopArr)] + "px";
}
};
LayoutSetup();
// 2. 레이아웃 함수를 뷰포트 사이즈 변경 이벤트에 실행
window.addEventListener("resize", LayoutSetup);
}
MasonryLayout(".list-container", ".list-item");
자바스크립트
function MasonryLayout(listContainerQuery, listItemQuery) {
const elemContainer = document.querySelector(listContainerQuery);
const elemFirstItem = elemContainer.querySelector(listItemQuery);
const getArrayMinIndex = (arr) => arr.reduce((r, v, i, a) => (v >= a[r] ? r : i), -1);
const getArrayMaxIndex = (arr) => arr.reduce((r, v, i, a) => (v <= a[r] ? r : i), -1);
const waitImageLoad = async (elem) => {
for (const img of elem.querySelectorAll("img"))
await img.decode();
};
const LayoutSetup = async () => {
const widthWrapper = elemContainer.offsetWidth;
const widthItem = elemFirstItem.offsetWidth;
let layoutTopArr = new Array(parseInt(`${widthWrapper / widthItem}`));
layoutTopArr.fill(0);
for (const elem of elemContainer.querySelectorAll(listItemQuery)) {
await waitImageLoad(elem);
let topMinIndex = getArrayMinIndex(layoutTopArr);
elem.style.left = topMinIndex * widthItem + "px";
elem.style.top = layoutTopArr[topMinIndex] + "px";
elem.classList.add("show");
layoutTopArr[topMinIndex] += elem.offsetHeight;
elemContainer.style.height =
layoutTopArr[getArrayMaxIndex(layoutTopArr)] + "px";
}
};
LayoutSetup();
window.addEventListener("resize", LayoutSetup);
}
MasonryLayout(".list-container", ".list-item");
반응형
'프로그래밍 > 웹 프로그래밍' 카테고리의 다른 글
포트폴리오를 위한 깃허브 페이지: 사이트맵과 SASS 사용 (0) | 2022.04.30 |
---|---|
포트폴리오를 위한 깃허브 페이지: 템플릿에 Jekyll SEO Tag 사용하기 (0) | 2022.04.30 |
포트폴리오를 위한 깃허브 페이지: VSCode 개발 설정 (0) | 2022.04.29 |
포트폴리오를 위한 깃허브 페이지: 로컬 테스트 환경 구성 (0) | 2022.04.28 |
포트폴리오를 위한 깃허브 페이지: WSL 개발 환경 구성 (0) | 2022.04.27 |
댓글