Container Query 실습: 같은 카드를 본문과 사이드바에 배치하기

··설명 약 8분·작성: 해비·블로그 소개

CSS Container Query로 같은 카드를 본문과 사이드바에 다르게 배치하려면 어떻게 하나요?

Container Query는 카드 부모의 폭을 기준으로 배치를 바꿉니다. 같은 화면에서도 넓은 본문은 가로형, 좁은 사이드바는 세로형으로 만들 수 있습니다. 부모의 기준과 자식의 조건부 스타일을 연결합니다.

적용 조건·범위: 800px 이상 화면에서 본문과 사이드바 카드에 서로 다른 열 구성이 나타날 수 있습니다. 본문 영역을 좁혀 @container 조건이 해제되는지 DevTools에서 확인하세요. 지원되지 않는 브라우저에서도 기본 세로 카드가 유지되어야 합니다. 실제 브라우저별 지원 검증은 별도입니다.

본문 근거: developer.mozilla.org/en-US/docs/Web/CSS/Guides/Containment/Container_queries

독립 실습: 직접 만들고 확인하기

Container Query 실습: 같은 카드를 본문과 사이드바에 배치하기 · 독립 실습

같은 카드가 본문과 사이드바에서 서로 다른 배치로 표시되는지 확인하세요.

예제 크게 보기 (새 탭)
.service-layout { display: grid; gap: 24px; }
@media (min-width: 800px) { .service-layout { grid-template-columns: minmax(0, 1fr) 260px; } }
.feature-slot { container-type: inline-size; }
.feature-card { display: grid; gap: 12px; padding: 16px; border: 1px solid #777; }
@container (min-width: 30rem) { .feature-card { grid-template-columns: 160px minmax(0, 1fr); } }
직접 실행: 환경 준비와 전체 파일

독자 질문

같은 카드가 넓은 본문과 좁은 사이드바에 있을 때, 화면 폭이 같아도 서로 다른 레이아웃으로 만들 수 있을까요?

직접 만들 화면

본문 카드는 가로형, 사이드바 카드는 세로형으로 배치되는 동일한 기능 카드

새 폴더에 index.html과 style.css를 만들고 아래 전체 파일을 각각 저장하세요. 브라우저에서 index.html을 엽니다.

<!doctype html>
<html lang="ko">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Container Query 실습: 같은 카드를 본문과 사이드바에 배치하기</title><link rel="stylesheet" href="./style.css"></head>
<body>
<div class="service-layout">
  <main class="main-area">
    <div class="feature-slot"><article class="feature-card"><h2>빠른 입력</h2><p>본문 카드의 설명입니다.</p></article></div>
  </main>
  <aside class="side-area" aria-label="관련 기능">
    <div class="feature-slot"><article class="feature-card"><h2>빠른 입력</h2><p>사이드바 카드의 설명입니다.</p></article></div>
  </aside>
</div>
</body>
</html>

확인할 결과와 실제 검증 상태

800px 이상 화면에서 본문과 사이드바 카드에 서로 다른 열 구성이 나타날 수 있습니다. 본문 영역을 좁혀 @container 조건이 해제되는지 DevTools에서 확인하세요. 지원되지 않는 브라우저에서도 기본 세로 카드가 유지되어야 합니다. 실제 브라우저별 지원 검증은 별도입니다.

실제 브라우저·DevTools 조작 결과: 미확인. 위 내용은 코드에서 예상되는 결과와 독자가 수행할 확인 절차입니다.

다음 학습 연결

11회차에서는 Container Query의 지원 범위를 프로젝트 대상 브라우저 기준으로 판단합니다.

Media Query

@media (min-width: 48rem) {
  .feature-card {
    display: flex;
  }
}

브라우저 viewport가 48rem 이상인지 봅니다.

페이지 전체 shell, header, 큰 Grid 같은 구조에 적합합니다.

Container Query

먼저 query container를 만듭니다.

.feature-slot {
  container-type:
    inline-size;
}

그 안의 card:

@container (
  min-width: 30rem
) {
  .feature-card {
    display: grid;
    grid-template-columns:
      160px
      minmax(0, 1fr);
    gap: 20px;
  }
}

viewport가 아니라 .feature-slot의 inline size를 기준으로 동작합니다.

같은 카드, 다른 배치

<main>
  <div class="feature-slot">
    <article class="feature-card">
      ...
    </article>
  </div>
</main>


<aside>
  <div class="feature-slot">
    <article class="feature-card">
      ...
    </article>
  </div>
</aside>

main에서는 넓어서 2열, sidebar에서는 좁아서 세로 카드가 될 수 있습니다.

컴포넌트가 자신이 놓인 문맥에 적응합니다.

Container Query는 화면 전체 폭이 아니라 지정한 컨테이너의 크기를 기준으로 카드 내부 배치를 바꿉니다.
개념 설명용 일러스트. Container Query는 화면 전체 폭이 아니라 지정한 컨테이너의 크기를 기준으로 카드 내부 배치를 바꿉니다. 이미지 원본 열기

container-name

여러 container가 중첩될 때 이름을 붙일 수 있습니다.

.feature-slot {
  container-name:
    feature;
  container-type:
    inline-size;
}
@container feature (
  min-width: 30rem
) {
  ...
}

간단한 구조에서는 이름 없이 사용해도 됩니다.

container-type:size와 inline-size

일반적인 가로 폭 반응형은 inline-size가 적절합니다.

size는 block dimension까지 포함한 size query가 필요할 때 사용합니다.

필요하지 않은 containment를 추가하지 않습니다.

지원 범위

Container size queries는 현재 주요 브라우저에서 널리 사용할 수 있는 기능입니다.

하지만 프로젝트 지원 브라우저가 오래된 환경까지 포함한다면 fallback을 준비할 수 있습니다.

기본 CSS를 먼저 작성하고 Container Query를 enhancement로 추가하는 구조가 안전합니다.

.feature-card {
  display: block;
}


@container (
  min-width: 30rem
) {
  .feature-card {
    display: grid;
  }
}

지원하지 않는 환경에서는 기본 block layout이 남습니다.

자주 발생하는 오류

@container를 썼는데 반응하지 않습니다

조상에 container-type이 있는지 확인합니다.

viewport를 줄여야만 테스트합니다

Container Query는 해당 부모 영역의 폭을 바꿔 테스트해야 합니다.

모든 section을 container로 만듭니다

재사용 컴포넌트가 실제 배치 공간에 따라 달라져야 하는 곳을 중심으로 사용합니다.

학습 완료 체크리스트

  • Media Query와 Container Query를 구분할 수 있습니다.
  • container-type:inline-size를 설정할 수 있습니다.
  • @container 조건을 작성할 수 있습니다.
  • 같은 컴포넌트를 main/sidebar에서 다르게 배치할 수 있습니다.
  • 기본 fallback을 유지한 progressive enhancement를 구성할 수 있습니다.

독립 실습은 여기서 마칠 수 있습니다. 아래 선택 실습은 TaskFlow 프로젝트 파일로 진행합니다.

선택 실습: 할 일 앱에 적용하기

TaskFlow는 간단한 서비스 화면에서 시작해 할 일 앱으로 이어 만드는 예제입니다. 이 선택 실습에서는 HTML과 CSS로 이번 화면을 만듭니다.

같은 기능 카드를 main과 sidebar에 넣고 부모 폭에 따라 내부 레이아웃이 바뀌도록 합니다.

선택: 연재 프로젝트와 전체 자료

연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.

TaskFlow 프로젝트 적용

Container Query 실습: 같은 카드를 본문과 사이드바에 배치하기 · 단계별 TaskFlow

TaskFlow 누적 10단계입니다. 같은 카드가 본문과 사이드바에서 서로 다른 배치로 표시되는지 확인하세요.

예제 크게 보기 (새 탭)

실행 가능한 시작본과 이번 단계

이번 단계 시작 ZIP을 새 폴더에 압축 해제합니다. 이 ZIP은 직전 단계의 완료 파일과 같으며 앞 글의 코드를 따로 모을 필요가 없습니다. 이번 단계 완료 ZIP은 다른 폴더에 풀어 비교합니다.

기존 main을 유지하면서 service-layout 안에 넓은 본문과 aside를 만듭니다. 동일한 카드가 각각 feature-slot 아래에 놓입니다.

  1. index.html: 이 파일의 기존 내용을 아래 완료 파일 전체와 대조하여 교체합니다. 다른 파일은 유지합니다.
  2. style.css: 이 파일의 기존 내용을 아래 완료 파일 전체와 대조하여 교체합니다. 다른 파일은 유지합니다.

짧은 코드 예제는 변경 위치를 설명합니다. 파일 전체를 작성하거나 교체할 때는 완료 파일 전체를 사용하세요. 생략 표시가 있는 부분 코드를 파일 전체로 저장하지 않습니다.

실행: ZIP의 index.html을 브라우저에서 엽니다. 외부 서버나 npm 설치 없이 로컬 HTML·CSS·이미지를 사용합니다. 문의 폼은 서버 전송을 연결하지 않은 문서 예제입니다. 연습 값만 사용하고 입력 형식을 확인합니다.

직접 확인할 결과: main은 하나이고 같은 카드가 넓은/좁은 부모에 배치됩니다. @container가 없으면 기본 세로 카드로 읽힙니다.

전체 파일·코드 보기

완료 파일 전체

index.html 전체 파일

<!DOCTYPE html>
<html lang="ko">
 <head>
  <meta charset="utf-8"/>
  <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
  <title>
   TaskFlow
  </title>
  <link href="./style.css" rel="stylesheet"/>
 </head>
 <body>
  <header class="site-header">
   <div class="page site-header__inner">
    <a class="brand" href="#home">
     TaskFlow
    </a>
    <nav aria-label="주요 메뉴" class="nav">
     <a href="#features">
      기능
     </a>
     <a href="#contact">
      문의
     </a>
    </nav>
   </div>
  </header>
  <main class="page" id="home">
   <div class="service-layout">
    <div>
     <section class="hero">
      <h1>
       해야 할 일을 한눈에 정리하세요
      </h1>
      <p>
       TaskFlow는 오늘의 작업을 빠르게 정리하는 서비스입니다.
      </p>
      <img alt="TaskFlow 서비스 소개 페이지의 기능·문의 화면" class="hero__image" height="800" src="./images/hero.webp" width="1200"/>
     </section>
     <section id="features">
      <h2>
       주요 기능
      </h2>
      <div class="feature-grid">
       <article class="card feature-card">
        <span class="feature-card__badge">
         NEW
        </span>
        <h3>
         빠른 작업 추가
        </h3>
        <p>
         해야 할 일을 바로 기록할 수 있습니다.
        </p>
        <img alt="TaskFlow 기능 카드 세 개" class="feature-card__image" height="450" src="./images/feature-board.webp" width="800"/>
       </article>
       <article class="card">
        <h3>
         간단한 분류
        </h3>
        <p>
         해야 할 일을 구분합니다.
        </p>
       </article>
       <article class="card">
        <h3>
         진행 확인
        </h3>
        <p>
         완료 여부를 확인합니다.
        </p>
       </article>
       <article class="card task-preview">
        <div class="task-preview__content">
         <h3>
          아주 긴 작업 제목과 URL
         </h3>
         <p>
          https://example.com/very-long-task-url-without-normal-break-points-123456789
         </p>
        </div>
        <a href="#contact">
         문의
        </a>
       </article>
      </div>
     </section>
     <section class="faq" id="faq">
      <h2>
       자주 묻는 질문
      </h2>
      <details>
       <summary>
        TaskFlow는 어떤 서비스인가요?
       </summary>
       <p>
        해야 할 일을 빠르게 정리하는 작업 관리 서비스 예제입니다.
       </p>
      </details>
      <details>
       <summary>
        모바일에서도 볼 수 있나요?
       </summary>
       <p>
        작은 화면부터 설계한 반응형 레이아웃을 사용합니다.
       </p>
      </details>
      <details>
       <summary>
        문의는 어디서 하나요?
       </summary>
       <p>
        아래 문의 폼을 이용할 수 있습니다.
       </p>
      </details>
     </section>
     <section id="pricing">
      <h2>
       요금제
      </h2>
      <div class="pricing-grid">
       <article class="card">
        <h3>
         Basic
        </h3>
        <p>
         개인 학습용
        </p>
       </article>
       <article class="card">
        <h3>
         Pro
        </h3>
        <p>
         더 많은 프로젝트
        </p>
       </article>
      </div>
      <div aria-label="요금제 비교표 가로 스크롤" class="table-scroll" role="region" tabindex="0">
       <table class="pricing-table">
        <caption>
         TaskFlow 요금제 기능 비교
        </caption>
        <thead>
         <tr>
          <th scope="col">
           항목
          </th>
          <th scope="col">
           Basic
          </th>
          <th scope="col">
           Pro
          </th>
         </tr>
        </thead>
        <tbody>
         <tr>
          <th scope="row">
           프로젝트
          </th>
          <td>
           3개
          </td>
          <td>
           무제한
          </td>
         </tr>
         <tr>
          <th scope="row">
           작업 보관
          </th>
          <td>
           30일
          </td>
          <td>
           무제한
          </td>
         </tr>
         <tr>
          <th scope="row">
           팀 공유
          </th>
          <td>
           없음
          </td>
          <td>
           지원
          </td>
         </tr>
        </tbody>
       </table>
      </div>
     </section>
     <section id="contact">
      <h2>
       문의하기
      </h2>
      <form>
       <div class="form-field">
        <label for="name">
         이름
        </label>
        <input autocomplete="name" id="name" name="name" type="text"/>
       </div>
       <div class="form-field">
        <label for="email">
         이메일
        </label>
        <input autocomplete="email" id="email" name="email" type="email"/>
       </div>
       <div class="form-field">
        <label for="message">
         문의 내용
        </label>
        <textarea id="message" name="message" rows="5"></textarea>
       </div>
       <button type="submit">
        문의 보내기
       </button>
      </form>
     </section>
     <div class="feature-slot">
      <article class="card feature-card">
       <img alt="TaskFlow 기능 카드 세 개" class="feature-card__image" height="450" src="./images/feature-board.webp" width="800"/>
       <div>
        <h3>
         작업 카드
        </h3>
        <p>
         넓은 공간에서는 이미지와 설명을 나란히 배치합니다.
        </p>
       </div>
      </article>
     </div>
    </div>
    <aside aria-label="기능 미리보기">
     <div class="feature-slot">
      <article class="card feature-card">
       <img alt="TaskFlow 기능 카드 세 개" class="feature-card__image" height="450" src="./images/feature-board.webp" width="800"/>
       <div>
        <h3>
         작업 카드
        </h3>
        <p>
         넓은 공간에서는 이미지와 설명을 나란히 배치합니다.
        </p>
       </div>
      </article>
     </div>
    </aside>
   </div>
  </main>
  <footer class="site-footer">
   <div class="page">
    <p>
     © TaskFlow
    </p>
   </div>
  </footer>
  <a class="help-button" href="#contact">
   문의
  </a>
 </body>
</html>

style.css 전체 파일

:root {
  color: #171717;
  background: #f5f5f5;
}


*,
*::before,
*::after {
  box-sizing: border-box;
}


body {
  margin: 0;
  font-family:
    Arial,
    sans-serif;
  line-height: 1.6;
  color: #171717;
  background: #f5f5f5;
}


.page {
  width: min(
    100% - 32px,
    960px
  );
  margin-inline: auto;
}


.site-header,
.site-footer {
  padding-block: 20px;
}


main {
  padding-block: 48px;
}


section + section {
  margin-top: 64px;
}


.card {
  padding: 24px;
  border: 1px solid #d7d7d7;
  background: #fff;
}


form {
  display: grid;
  gap: 16px;
}


.form-field {
  display: grid;
  gap: 8px;
}


input,
textarea {
  width: 100%;
  min-height: 44px;
  padding: 10px 12px;
  border: 1px solid #b8b8b8;
  background: #fff;
}


button {
  min-height: 44px;
  padding: 10px 16px;
  border: 1px solid #171717;
  background: #171717;
  color: #fff;
}

a { color: inherit; }
input, textarea, button { font: inherit; }
h1 { font-size: 32px; font-weight: 700; }
h2 { font-size: 24px; }
h3 { font-size: 20px; }

.site-header__inner {
  display: flex;
  flex-direction: column;
  gap: 12px;
}


.nav {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 16px;
}


.feature-list {
  display: flex;
  flex-direction: column;
  gap: 16px;
}


@media (min-width: 48rem) {
  .site-header__inner {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
  }


  .feature-list {
    flex-direction: row;
  }


  .feature-list > * {
    flex: 1;
  }
}

.site-header {
  position: sticky;
  top: 0;
  z-index: 10;
  background: #f5f5f5;
  border: 1px solid #ddd;
}


.feature-card {
  position: relative;
}


.feature-card__badge {
  position: absolute;
  top: 12px;
  right: 12px;
  padding: 4px 8px;
  border: 1px solid #171717;
  background: #fff;
  font-size: 12px;
}


.help-button {
  position: fixed;
  right: 16px;
  bottom: 16px;
  min-width: 44px;
  min-height: 44px;
  padding: 10px 14px;
  border: 1px solid #171717;
  background: #171717;
  color: #fff;
  text-decoration: none;
}

.faq {
  display: grid;
  gap: 12px;
}


.faq details {
  border: 1px solid #d7d7d7;
  background: #fff;
}


.faq summary {
  padding: 16px;
  cursor: pointer;
  font-weight: 700;
}


.faq details p {
  margin: 0;
  padding: 0 16px 16px;
}


.faq summary:focus-visible {
  outline: 2px solid #171717;
  outline-offset: 2px;
}

.feature-grid,
.pricing-grid {
  display: grid;
  grid-template-columns:
    repeat(
      auto-fit,
      minmax(
        min(100%, 220px),
        1fr
      )
    );
  gap: 16px;
}

.task-preview {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}


.task-preview__content {
  min-width: 0;
  flex: 1;
}


.task-preview__content p {
  overflow-wrap: anywhere;
}

.settings-layout {
  display: grid;
  grid-template-columns:
    180px
    minmax(0, 1fr);
  gap: 24px;
}

.table-scroll {
  overflow-x: auto;
}


.pricing-table {
  width: 100%;
  min-width: 640px;
  border-collapse: collapse;
  background: #fff;
}


.pricing-table caption {
  margin-bottom: 12px;
  text-align: left;
  font-weight: 700;
}


.pricing-table th,
.pricing-table td {
  padding: 12px;
  border: 1px solid #d7d7d7;
  text-align: left;
}

img {
  display: block;
  max-width: 100%;
}


.hero__image {
  width: 100%;
  height: auto;
}


.feature-card__image {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

.service-layout {
  display: grid;
  gap: 24px;
}


@media (
  min-width: 64rem
) {
  .service-layout {
    grid-template-columns:
      minmax(0, 1fr)
      280px;
  }
}

.feature-slot {
  container-type:
    inline-size;
}


.feature-card {
  display: grid;
  gap: 16px;
}


@container (
  min-width: 30rem
) {
  .feature-card {
    grid-template-columns:
      180px
      minmax(0, 1fr);
    align-items: center;
  }
}

.service-layout > * { min-width: 0; }

위 파일과 ZIP의 동명 파일은 동일합니다. 회차별 README·CHANGES 문서는 ZIP에 포함합니다. 생성 산출물·node_modules·실제 환경 변수 파일은 포함하지 않습니다.

index.html

<div class="service-layout">
  <main>
    <div class="feature-slot">
      <article class="card feature-card">
        <img
          class="feature-card__image"
          src="./images/feature-board.webp"
          alt="카드 형태 작업 목록"
          width="800"
          height="450"
        />


        <div>
          <h3>작업 카드</h3>
          <p>
            넓은 공간에서는 이미지와 설명을 나란히 배치합니다.
          </p>
        </div>
      </article>
    </div>
  </main>


  <aside>
    <div class="feature-slot">
      <article class="card feature-card">
        ...
      </article>
    </div>
  </aside>
</div>

style.css

페이지 전체:

.service-layout {
  display: grid;
  gap: 24px;
}


@media (
  min-width: 64rem
) {
  .service-layout {
    grid-template-columns:
      minmax(0, 1fr)
      280px;
  }
}

component container:

.feature-slot {
  container-type:
    inline-size;
}


.feature-card {
  display: grid;
  gap: 16px;
}


@container (
  min-width: 30rem
) {
  .feature-card {
    grid-template-columns:
      180px
      minmax(0, 1fr);
    align-items: center;
  }
}

main의 feature-slot은 충분히 넓으면 2열이 되고 sidebar의 동일 카드에는 조건이 적용되지 않을 수 있습니다.

시작본과 이번 변경

HTML CSS 이미지 문제 해결 실습: 경로·비율·잘림·흐림·CLS의 완료 체크포인트를 시작본으로 사용합니다.

index.html의 기존 main을 중복 생성하지 말고 service-layout 안 본문·aside 카드 위치에 적용합니다. style.css의 feature-slot에 container-type을, 자식 card에 @container 규칙을 추가합니다. 창 폭을 유지한 채 부모 폭을 바꿔 확인합니다.

연속 프로젝트 선수 조건

  • media query와 Grid/Flex 반응형을 알고 있어야 합니다.
  • 같은 컴포넌트가 서로 다른 폭의 부모에 배치되는 상황을 가정합니다.

이전 체크포인트

publishing-step-09-images

이번 목표

같은 기능 카드를 main과 sidebar에 넣고 부모 폭에 따라 내부 레이아웃이 바뀌도록 합니다.

실행 확인

  • viewport를 넓게 둔 상태에서 main/sidebar 카드가 서로 다른 layout인지 확인합니다.
  • sidebar width만 바뀌어도 카드 내부가 반응하는지 확인합니다.
  • 지원하지 않는 환경에서도 기본 세로 layout을 사용할 수 있는지 확인합니다.

완료 체크포인트

publishing-step-10-container-query

다음 학습

다음 글에서는 Container Query를 포함해 현재 사용한 CSS 기능이 어느 브라우저 범위에서 안전한지 Baseline과 MDN을 이용해 판단합니다.

공식 문서

글 소개·학습 준비·전체 목차
Container Query 실습: 같은 카드를 본문과 사이드바에 배치하기 대표 일러스트

웹 퍼블리싱 · 10 / 15 · 필수 학습 · Container Query

Media Query는 viewport를 기준으로 레이아웃을 바꿉니다.

하지만 같은 카드가 넓은 main과 좁은 sidebar에 동시에 들어가면 viewport는 같아도 카드가 사용할 수 있는 실제 공간은 다릅니다.

Container Query는 부모 컨테이너 크기를 기준으로 자식 스타일을 바꿀 수 있게 합니다.

이번 글에서 배우는 내용

  • media query와 container query 차이
  • container-type:inline-size
  • @container
  • container-name
  • 재사용 컴포넌트에 Container Query가 유리한 이유
  • fallback을 판단하는 기준

먼저 알아야 할 내용

CSS Grid/Flex와 media query, 부모·자식 구조를 알면 됩니다. 같은 창에서 부모 폭만 다른 카드 두 개로 실습합니다.

목차

이 글이 도움이 되었나요?

조회 중

읽음 기록

읽음 — / 전체 15개 · 필수 13개

이 글을 쓴 해비

웹사이트를 제작·운영하고, 개인 서비스와 자동화·AI 도구를 만듭니다. 직접 만들며 배운 내용과 문제 해결 과정을 BlogFlow에 기록합니다.

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기