Devin.KR

Flex와 Grid로 목록 배치

100분 안팎

학습 목표

콘텐츠에 맞게 목록과 도구 모음의 배치를 정합니다.

개념

왜 도구 모음과 카드 목록을 다르게 배치하나요

검색어 입력, 지역 선택, 검색 버튼은 하나의 작업 흐름입니다. 공간이 줄면 같은 순서를 유지하며 다음 줄로 내려가야 합니다. 카드 목록은 여러 행의 열 간격을 맞춰 비교하기 쉽도록 배치합니다. 도구 모음에는 Flex를, 목록에는 Grid를 사용해 각 배치의 책임을 분리합니다. 속성 이름을 외우기보다 좁아졌을 때 어떤 관계를 유지할지 먼저 정합니다.

앞 레슨의 상자 크기 계산을 이어 사용합니다. 검색 폼을 화면에 맞춘다고 label이나 name을 지우지 않습니다. starter는 실제 label과 입력을 field 묶음으로 감싼 상태입니다. 이 묶음은 레이블과 입력이 함께 이동하도록 만드는 표현 단위입니다. 검색어→지역→검색 버튼이라는 DOM 순서를 유지하고 화면 배치만 바꿉니다.

Flex가 맡는 관계

display:flex를 지정한 부모의 직접 자식이 Flex 아이템이 됩니다. 여기서는 검색어 field, 지역 field, 버튼입니다. 기본 한 줄 배치에 flex-wrap:wrap을 더하면 공간이 부족할 때 아이템이 다음 줄로 내려갑니다. gap은 아이템 사이 간격을 만듭니다. 각 아이템 바깥 여백으로 같은 간격을 흉내 내면 끝쪽에도 불필요한 공간이 생길 수 있습니다.

field의 flex:1 1 14rem은 성장 허용, 축소 허용, 기준 크기를 차례로 나타냅니다. 14rem은 절대 최소 너비를 뜻하지 않습니다. 가용 공간이 작으면 줄어들 수 있어야 합니다. Flex 아이템의 자동 최소 크기가 콘텐츠 때문에 축소를 막는 경우를 고려해 field에 min-width:0을 둡니다. 입력과 select에도 width:100%와 min-width:0을 적용해 묶음 안에서 맞춥니다.

field 자체도 Flex 컨테이너로 만들어 flex-direction:column을 지정합니다. 레이블 위, 입력 아래의 관계를 유지하기 위한 것입니다. 바깥 form의 주축은 가로, 안쪽 field의 주축은 세로입니다. align-items:end는 이 예시의 바깥 주축과 교차축 관계에서 버튼을 필드 하단에 맞춥니다. 레이블이 길어질 때도 입력 이름은 사라지지 않습니다.

Grid가 맡는 관계

cards에 display:grid를 지정하면 카드들이 같은 열 규칙을 공유합니다. 기본 배치는 grid-template-columns:minmax(0,1fr)로 한 열입니다. 작은 화면에서 목록을 먼저 읽을 수 있는 상태를 만들고 충분한 폭에서 두 열로 확장합니다. 작은 화면 규칙이 기본이고 큰 화면 규칙이 추가되는 방식을 쓰면 기본 문서가 자연스럽게 읽힙니다.

1fr은 남는 공간을 분배하는 비율입니다. repeat(2,minmax(0,1fr))는 두 열이 남은 공간을 같은 비율로 나누며 각 트랙의 최소 크기를 0으로 둡니다. 카드 안의 내용 처리까지 자동으로 완료된다는 뜻은 아닙니다. 긴 주소의 줄바꿈과 카드 최소 너비는 다음 레슨에서 별도로 확인합니다. 트랙, 아이템, 콘텐츠의 책임을 섞지 않습니다.

두 열 사이에 1rem gap이 있으면 그 간격을 먼저 제외한 공간을 두 열이 나눕니다. width:50%인 카드 두 개에 간격을 추가하는 방식은 총합이 부모보다 커지기 쉽습니다. Grid에 열 계산을 맡기고 카드에는 자신의 열을 채우는 너비를 둡니다. 행마다 카드 높이가 달라도 제목과 링크를 잘라 맞추지 않습니다. 내용에 따라 행 높이가 늘어나게 둡니다.

분기점은 콘텐츠로 정합니다

이 실습은 min-width:48rem에서 두 열로 바꿉니다. 미디어 쿼리가 참조하는 초기 글꼴 크기가 16px일 때 768 CSS px에 해당하는 예시 값입니다. 페이지 CSS에서 루트 font-size를 바꾸는 것과 미디어 쿼리의 rem 기준을 구분합니다. 특정 장치 이름을 뜻하는 값은 아닙니다. 두 열에서 카드가 지나치게 답답해지는지, 폼이 어떤 줄로 내려가는지 보며 분기점을 설명합니다. 경계 바로 아래와 경계에서 모두 확인해야 누락된 구간을 찾을 수 있습니다.

320·390·768·1280px 네 폭만 통과했다고 모든 중간 폭이 안전한 것은 아닙니다. 너비를 연속으로 줄여 보며 행이 바뀌는 지점을 확인합니다. 767과 768px도 기본 글꼴 설정에서 비교합니다. 사용자 글꼴 설정에 따라 rem 분기점의 픽셀 환산이 달라질 수 있으므로 실제 media 규칙 적용 여부를 개발자 도구에서 읽습니다.

검색 버튼을 absolute 위치로 오른쪽에 고정하면 폼 내용이 늘어날 때 겹칠 수 있습니다. 이 레슨은 일반 문서 흐름과 Flex 줄바꿈을 사용합니다. order로 버튼을 앞으로 보내면 화면 순서와 키보드 순서가 달라질 수 있습니다. 기능 흐름이 바뀌는 요구가 아니라면 DOM 순서를 유지하고 줄바꿈과 간격으로 해결합니다.

어떻게 디버깅하나요

개발자 도구에서 form을 선택하고 Flex 표시를 켜 직접 자식을 확인합니다. 입력만 Flex 아이템일 것이라고 생각했는데 field가 선택된다면 HTML 묶음을 다시 봅니다. cards를 선택한 뒤 Grid 오버레이로 열과 gap을 확인합니다. display:grid를 카드 자신에게 넣는 것은 카드 내부 배치만 바꾸므로 목록의 열 수를 고치지 못합니다.

FAIL 한 열 기본값과 두 열 분기는 기본 선택자와 media 규칙을 확인하라는 단서입니다. media 조건이 min-width인지 max-width인지, 중괄호가 닫혔는지, 단위가 빠지지 않았는지 살핍니다. 잘못된 CSS 선언은 브라우저가 무시하고 나머지를 적용할 수 있어 콘솔 오류가 없다고 정상이라고 판정하지 않습니다. 취소된 선언과 실제 열 폭을 함께 봅니다.

Flex 줄바꿈은 각 행을 독립적으로 배치합니다. 첫 행에 필드 두 개가 있고 둘째 행에 버튼만 있다면 둘째 행의 폭 분배는 첫 행과 따로 계산됩니다. 여러 행에서 세로 열 경계를 꼭 맞춰야 하는 요구라면 Grid가 더 직접적입니다. 반대로 도구 모음처럼 항목 크기와 줄 이동이 중요하면 Flex로 관계를 표현할 수 있습니다. 어떤 속성이 더 최신인지보다 정렬해야 할 축과 행 사이 관계로 선택합니다.

완료 후 설명할 수 있는 것

Grid 오버레이에서 두 열의 폭을 비교할 때 컨테이너 전체 폭을 단순히 반으로 나누지 않습니다. 먼저 좌우 padding과 열 사이 gap을 제외한 공간을 구합니다. 예를 들어 내용 폭이 720px이고 gap이 16px라면 동일한 두 열은 각각 352px입니다. 카드 border와 padding은 그 열 안에서 계산합니다. 폼의 줄바꿈과 목록의 열 전환은 서로 다른 조건으로 일어나므로 같은 폭에서 함께 바뀌어야 한다고 가정하지 않습니다.

동료에게 도구 모음은 아이템이 순서대로 흐르며 줄을 바꾸고, 목록은 열 규칙을 공유한다고 설명합니다. Grid를 쓰면 Flex를 쓸 수 없다는 뜻은 아닙니다. 같은 화면 안에서 서로 다른 부모가 다른 역할을 맡습니다. 첫 카드에 긴 제목을 넣어도 다른 카드와 간격이 유지되는지 보고 다음 레슨에서 텍스트 넘침을 분리해 해결합니다.

자동 최소 크기의 세부 원리는 MDN min-width 설명과 더 읽기의 서재 예시로 확인합니다. 여기서는 행사 폼과 목록에서 직접 부모를 선택하고 두 레이아웃의 역할을 설명하는 데 집중합니다. 완료 자료에는 수정한 CSS, 검사 결과, 관찰한 줄바꿈 위치를 함께 남깁니다.

따라하기

starter와 실패 조건 확인

이 레슨 starter 압축을 새 폴더에 풀고 package.json이 있는 폴더에서 실행합니다. Node/npm과 Python 3가 필요하며 외부 의존 패키지는 없습니다.

npm ci
npm test

starter는 일부 CSS 계약이 FAIL인 상태입니다. check.py와 document-check.py는 고치지 않습니다. ENOENT는 작업 폴더, python3 실행 실패는 Python 설치부터 확인합니다.

해당 단계의 CSS 규칙 교체

assets/site.css 마지막 TODO 실패 블록을 다음 코드로 교체합니다. 이전 단계와 기본 글꼴·포커스 선언은 유지합니다. 동일 선택자의 실패 선언을 뒤에 남기지 않습니다.

nav { display: flex; flex-wrap: wrap; gap: 1rem; }
.search-form { display: flex; flex-wrap: wrap; align-items: end; gap: 1rem; }
.field { display: flex; flex-direction: column; flex: 1 1 14rem; min-width: 0; }
input, select { width: 100%; min-width: 0; }
.cards { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem; }
@media (min-width: 48rem) { .cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

index.html의 기존 폼 이름과 레이블, GET 목적지는 그대로 둡니다. 저장 후 브라우저를 새로고침합니다.

solution 기준 자동 검사

실습 폴더에서 실행합니다. 아래 출력은 모범 답안을 실제 실행한 결과입니다. 이 검사는 제공 CSS 선언 계약과 앞 모듈 HTML 계약을 확인하며 렌더링 검증 결과는 아닙니다.

npm test

실행 결과

> frontend-m02-flex-grid@1.0.0 test
> python3 check.py

PASS 앞 모듈 HTML·폼 계약 18개 유지
PASS 긴 제목·URL·이미지 없는 카드 fixture
PASS index.html 뷰포트 및 공통 CSS
PASS event.html 뷰포트 및 공통 CSS
PASS results.html 뷰포트 및 공통 CSS
PASS 공통 border-box 및 카드 테두리
PASS 컨테이너 가변 너비와 여백
PASS 카드 padding을 포함하는 가변 너비
PASS 폼 Flex 줄바꿈과 필드 축소
PASS 목록 Grid와 gap
PASS 한 열 기본값과 두 열 분기
PASS 폼 이름 및 DOM 순서 보존
CSS 계약 12개, 실패 0개
화면·200% 확대·실제 Tab 표시는 LAYOUT.md에 별도 기록합니다.

실제 화면과 사용자 흐름 기록

브라우저의 파일 열기로 index.html을 엽니다. form의 Flex 직접 자식이 field·field·button인지 확인합니다. 320·390·768·1280px와 기본 글꼴의 767/768px 경계를 비교하며 Grid 열 수와 버튼 줄바꿈을 기록합니다.

콘솔에서 아래 숫자를 읽고 LAYOUT.md에 환경·페이지·폭·관찰을 남깁니다. 수치가 같아도 잘린 내용이나 포커스가 없는지 눈으로 확인합니다. 실행 전의 결과는 미확인으로 둡니다.

({scrollWidth: document.documentElement.scrollWidth,
  clientWidth: document.documentElement.clientWidth})

확인 문제

실습

검색 도구 모음과 행사 카드 목록을 Flex·Grid로 배치합니다. starter의 TODO CSS를 수정해 npm ci 후 npm test를 통과시키세요. 자동 검사는 제공 선언 계약만 확인합니다. HTML의 name·label·설명 참조·GET 제출·DOM 순서를 유지하고, 세 페이지 네 폭·200% 확대·키보드 실제 관찰을 LAYOUT.md에 기록합니다. 검사 파일은 수정하지 않습니다.

시작 코드·테스트 내려받기

실행 명령

npm test

기대 결과

CSS 계약 12개, 실패 0개. 화면·확대·키보드 결과는 LAYOUT.md에 별도 기록합니다.

모범 답안모범 답안 내려받기

더 읽기

면접 질문

  • 검색 도구 모음과 여러 행의 카드 목록에 각각 Flex와 Grid를 선택하는 이유는 무엇인가요?
  • flex-basis와 min-width는 어떤 차이가 있고, 줄바꿈에 어떻게 영향을 주나요?