모바일·데스크톱 레이아웃 가이드
Tailwind로 한 화면을 모바일과 데스크톱 양쪽에 맞출 때 쓰는 판단 기준을 모았어요. 작은 것에서 큰 것 순서로 읽어요. 한 줄 배치(1부), 섹션 구조(2부), 배치 말고 달라지는 것(3부) 순이에요.
- 버튼으로 방식을 바꾸고 "부모 폭" 슬라이더로 화면이 좁아지는 상황을 만들어요.
- 회색 점선은 부모 요소, 파란 점선은 묶음이에요.
- 데모는 창 폭이 아니라 부모 폭이 넓어질 때(520px 이상) 데스크톱 배치로 바뀌어요. 코드에서
lg:가 붙은 클래스가 그때 적용되는 부분이에요. - "HTML 코드 보기"를 펼치면 그 모형의 코드가 나와요. 클래스를 누르면 그 클래스가 꺼지고 모형에 바로 반영돼요. 눌러도 모형이 그대로면 그 이유가 코드 아래에 나와요.
- 모형은 코드에 적힌 태그와 클래스를 그대로 달고 있어요. 코드에 없는 건 색, 테두리, 글자 크기, 안쪽 여백 같은 꾸밈뿐이에요. 모형 안은 Tailwind의 기본 초기화(제목과 문단의 바깥 여백 0, 목록 점 없음)와
break-keep이 걸린 상태예요. - 폰에서는 화면이 좁아서 데모가 데스크톱 배치까지 넓어지지 못해요. 데스크톱 브라우저에서 보고, (호버와 터치)만 폰에서도 열어 보세요.
1부한 줄 배치하기: flex와 grid
요소 몇 개를 한 줄에 놓을 때 flex와 grid 중 무엇을 쓸지 정해요.
flex와 grid의 차이: 누가 폭을 정하나
어느 쪽이 더 좋은 게 아니에요. 아래 두 데모에서 한 번은 grid가, 한 번은 flex가 맞아요.
grid가 맞는 경우: 여러 행의 열을 맞출 때
랜딩 페이지의 반응 지수 막대와 같은 구조예요. 행마다 라벨 · 막대 · 숫자가 반복돼요.
해 보기버튼을 왼쪽부터 차례로 누르면서 막대의 시작점과 끝점을 보세요. 첫 라벨도 길게 바꿔 보세요.
행마다 실제 폭
HTML 코드 보기
클래스를 누르면 꺼지고, 다시 누르면 켜져요. 눌러도 모형이 그대로면 그 이유가 코드 아래에 나와요.
grid-cols-[6rem_1fr_3rem] 읽는 법
대괄호 안의 _는 공백이에요. CSS로는 grid-template-columns: 6rem 1fr 3rem, 곧 열 세 개의 폭이에요.
| 값 | 뜻 | 정하는 법 |
|---|---|---|
6rem | 라벨 열. 고정 폭 (보통 96px) | 가장 긴 라벨이 들어가고 조금 남는 폭 |
1fr | 막대 열. 남는 폭 전부 | 정하지 않아요. 부모 폭에서 나머지를 뺀 값이에요 |
3rem | 숫자 열. 고정 폭 (보통 48px) | 가장 긴 숫자 "100%"가 들어가는 폭 |
부모가 320px이고 간격이 12px이면 막대 열은 320 − 96 − 48 − 12 × 2 = 152px이에요. 위 데모에서 "grid (폭 고정)"을 고르고 부모 폭을 움직이면, 라벨과 숫자는 그대로고 막대 폭만 바뀌어요.
grid-cols-[6rem_1fr_3rem]를 반복해요. 폭을 직접 정해야 하고 더 긴 이름이 들어오면 다시 정해야 해요. 행이 서너 개고 데이터가 정해져 있으면 충분해요.grid-cols-[auto_1fr_auto]를 한 번 선언하고 각 행은 grid-cols-subgrid로 물려받아요. 가장 긴 라벨에 맞춰 열 폭이 스스로 정해져요. 데모의 네 번째 버튼이에요.flex가 맞는 경우: 폭이 내용마다 달라야 할 때
태그, 버튼 묶음, 내비게이션 링크가 여기에 해당해요.
해 보기버튼을 바꿔 가며 부모 폭을 줄여 보세요.
HTML 코드 보기
클래스를 누르면 꺼지고, 다시 누르면 켜져요. 눌러도 모형이 그대로면 그 이유가 코드 아래에 나와요.
고르는 기준
질문은 하나예요. 여러 행이나 칸의 폭을 서로 맞춰야 하나? 맞춰야 하면 grid, 내용 길이에 맡겨도 되면 flex예요.
| 상황 | 선택 | Tailwind 예 |
|---|---|---|
| 여러 행에서 열 폭이 같아야 함 | grid | grid grid-cols-[6rem_1fr_3rem] |
| 모바일 1열 → 데스크톱 2열 | grid | grid lg:grid-cols-2 |
| 한 줄 나열, 폭은 내용대로 | flex | flex flex-wrap gap-2 |
| 양끝으로 밀기 (로고 ↔ 메뉴) | flex | flex justify-between |
| 아이콘 + 글자 한 쌍 | flex | flex items-center gap-2 |
| 세로로 쌓기만 함 | 둘 다 됨 | grid gap-4 또는 flex flex-col gap-4 |
둘은 섞어 써요. 큰 틀은 grid로 나누고 그 안의 작은 나열은 flex로 놓는 경우가 많아요. 지금 dashboard.tsx도 2단 배치는 grid, 푸터의 양끝 정렬은 flex예요.
2부구조 잡기: 묶음
한 화면을 모바일과 데스크톱 양쪽에 맞추려면, 무엇을 무엇과 묶을지가 먼저 정해져야 해요.
묶음이 왜 필요한가
묶음은 요소 몇 개를 감싸는 부모 요소예요. 래퍼(wrapper)라고도 불러요. div일 때가 많고, 의미가 있으면 section, ul, figure를 써요.
묶음이 필요한 이유는 grid와 flex가 바로 아래 자식만 배치하기 때문이에요. "제목·설명·버튼은 왼쪽, 카드는 오른쪽"을 시키려면 제목·설명·버튼이 먼저 한 덩어리로 묶여 있어야 해요.
lg:grid-cols-2를 받는 자리예요.해 보기"묶음 없음"인 채로 부모 폭을 줄였다 넓혀 보세요. 그다음 "묶음 있음"으로 바꿔서 다시 해 보세요.
HTML 코드 보기
클래스를 누르면 꺼지고, 다시 누르면 켜져요. 눌러도 모형이 그대로면 그 이유가 코드 아래에 나와요.
묶음을 찾는 질문
묶음은 감각으로 정하는 게 아니에요. 대부분 시안에서 확인할 수 있는 질문 세 개로 정해져요. 감각처럼 보이는 건 이 질문에 여러 번 답해 본 사람이 빨리 답하는 거예요.
질문 1. 간격이 어디서 넓어지나
가장 기계적인 방법이에요. 요소 사이 간격을 재서 주변보다 눈에 띄게 넓은 곳이 묶음의 경계예요. 디자이너가 간격으로 이미 묶음을 표시해 둔 거예요.
해 보기숫자를 눌러 간격을 바꿔 보세요. 전부 같게도 만들어 보세요.
질문 2. 두 요소가 같이 다니나
요소 두 개를 골라 모바일 시안과 데스크톱 시안에서 비교해요.
| 두 요소가… | 그러면 |
|---|---|
| 모바일에서는 위아래로, 데스크톱에서는 좌우로 놓인다 (또는 그 반대) | 방향을 바꾸는 부모 아래에 둘을 둬요. 예: grid lg:grid-cols-2 |
| 붙은 채로 함께 움직여서 다른 덩어리의 옆으로 간다 | 둘을 감싸는 묶음을 만들어요. 그 묶음이 통째로 움직여요. |
| 배경, 테두리, 최대 폭을 같이 쓴다 | 그 배경이나 테두리를 가진 상자로 감싸요. |
| 합쳐서 한 단어로 부를 수 있다 (카드, 내비게이션, 목록) | 의미가 있는 요소로 감싸요. 예: figure, nav, ul |
| 어느 것도 아니다 | 묶지 않아요. 형제로 둬요. |
질문 3. 이 div를 지우면 뭐가 깨지나
반대 방향의 확인이에요. 이미 있는 div가 정말 필요한지 의심될 때 써요. 지워도 깨지는 게 없으면 묶음이 아니니 지워요.
해 보기경우마다 "div 지우기"를 눌러 보세요.
HTML 코드 보기
클래스를 누르면 꺼지고, 다시 누르면 켜져요. 눌러도 모형이 그대로면 그 이유가 코드 아래에 나와요.
질문으로 안 정해질 때
| 상황 | 정하는 방법 |
|---|---|
| 같은 결과를 내는 구조가 여러 개 | HTML이 덜 깊은 쪽을 골라요. |
| 간격이 비슷하거나 데스크톱 시안이 없음 | 의미로 묶어요. "제목 + 설명 + 버튼", "사진 + 설명글", "라벨 + 입력칸"처럼 한 가지를 설명하는 요소들은 데스크톱에서도 거의 항상 같이 움직여요. |
| 나중에 요소가 늘어날 것 같음 | 기획을 아는 만큼만 맞힐 수 있어요. 모르면 지금 시안대로 해요. |
여기서 틀려도 비용은 작아요. 간격을 부모가 갖게 해 두면() 묶음을 바꾸는 건 div 하나를 넣고 빼는 일이에요.
작업 순서: 구조는 먼저, 스타일은 모바일부터
모바일 화면을 다 만든 뒤 데스크톱에서 묶으려 하면 어려워요. 이미 맞춰 놓은 모바일 간격을 다시 손봐야 하기 때문이에요.
"모바일 우선"은 CSS를 쓰는 순서에 대한 규칙이에요. HTML 구조는 두 시안을 모두 보고 먼저 정해요.
| 무엇을 보고 정하나 | |
|---|---|
| HTML 구조 (묶음) | 모바일과 데스크톱 시안 둘 다 |
| CSS (클래스) | 모바일 먼저, 그다음 lg: |
해 보기1단계부터 차례로 눌러 보세요.
5단계에서 접두사가 없는 클래스는 모든 폭에 적용되니 모바일 스타일이 돼요. lg:가 붙은 클래스는 화면 폭 1024px 이상에서 그걸 덮어써요. 그래서 모바일을 먼저 완성하고 달라지는 것만 lg:로 더해요.
묶음을 코드로 쓸 때 지킬 세 가지
① 묶음 사이 간격은 부모가 갖기
묶음과 묶음 사이 간격을 자식의 margin으로 주면, 배치가 바뀔 때 그 여백이 엉뚱한 곳에 남아요. 부모의 gap으로 주면 세로로 쌓일 때는 위아래 간격이, 가로로 놓일 때는 좌우 간격이 돼요.
해 보기두 방식을 바꿔 가며 부모 폭을 줄였다 넓혀 보세요. 넓을 때 파란 점선 상자의 아래쪽을 보세요.
HTML 코드 보기
클래스를 누르면 꺼지고, 다시 누르면 켜져요. 눌러도 모형이 그대로면 그 이유가 코드 아래에 나와요.
묶음 안에서 요소마다 간격이 달라 gap 하나로 안 될 때는 뒤따르는 요소에 mt-*를 줘요. 방향은 위쪽 하나로만 쓰고 묶음의 마지막 요소 아래에는 여백을 두지 않아요. 지금 dashboard.tsx가 이 방식이에요. 글 묶음과 카드 사이는 gap-8 lg:gap-20, 글 묶음 안은 mt-5와 mt-1이에요.
② 배경은 바깥 요소에, 폭 제한과 배치는 안쪽 요소에
배경은 화면 끝까지 가야 하고 내용은 최대 폭 안에 있어야 해요. 한 요소가 둘을 다 할 수 없으니 나눠요.
<section class="bg-paper"> ← 배경: 화면 끝까지
<div class="container-page ← 폭 제한: 최대 폭과 좌우 여백
grid gap-8
lg:grid-cols-2 lg:gap-20"> ← 배치가 바뀌는 지점
<div>...</div> ← 묶음 1: 제목 · 설명 · 버튼
<figure>...</figure> ← 묶음 2: 카드
</div>
</section>
dashboard.tsx 맨 위 주석 "섹션은 전체 폭, 내용만 container-page"가 이 규칙이에요.
③ HTML 순서는 모바일에서 읽는 순서로
스크린리더와 탭 키는 화면에 보이는 위치가 아니라 HTML 순서를 따라요. 데스크톱은 CSS로 배치만 바꾸고 순서는 건드리지 않아요.
상자가 두 시안에서 안 맞을 때
모바일과 데스크톱의 모양이 가로·세로 전환보다 더 많이 다를 때예요. 방법이 세 가지 있어요. 위에서부터 시도해서 되는 첫 번째에서 멈춰요. 아래로 갈수록 관리할 것이 늘어나기 때문이에요.
| 방법 | 맞는 경우 | 주의할 점 |
|---|---|---|
| 1. CSS로 자리만 옮기기 | 요소는 같고 위치만 달라요. | 화면에 보이는 순서와 HTML 순서가 달라져요. 읽는 순서가 뒤바뀔 정도면 쓰지 않아요. |
| 2. 둘 다 두고 하나만 보이기 | 작은 조각의 모양이 달라요. 예: 햄버거 버튼 ↔ 가로 메뉴 | 큰 덩어리에 쓰면 같은 내용을 두 번 관리해요. |
| 3. JS로 하나만 그리기 | 모양보다 동작이 달라요. 예: 카드 목록 ↔ 표, 하단 시트 ↔ 팝오버 | 서버에서 화면을 미리 그리는 구조면 처음에 다른 쪽이 잠깐 보일 수 있어요. |
해 보기방법마다 부모 폭을 줄였다 넓혀 보세요.
HTML 코드 보기
클래스를 누르면 꺼지고, 다시 누르면 켜져요. 눌러도 모형이 그대로면 그 이유가 코드 아래에 나와요.
방법을 고른 뒤: 두 벌이 되는 범위 정하기
방법 2와 3은 같은 내용을 두 가지 모양으로 만들어요. 그래서 방법을 고른 다음에는 어디까지 두 벌로 만들지를 따로 정해요. 아래 두 방식은 방법 2·3 중 하나를 가리키는 게 아니라, 둘 모두에 해당하는 기준이에요.
hidden lg:block / lg:hidden, 방법 3이라면 isDesktop ? <DesktopPage /> : <MobilePage />예요. 빠르지만 고칠 때마다 두 곳을 고쳐야 해요.<ReactionCards rows={rows} />와 <ReactionTable rows={rows} />를 같이 두고 하나만 보이면 rows는 하나예요.방법 3이 방법 2보다 나은 것도 아니에요. 방법 2는 처음부터 맞는 쪽이 보이고 JS가 필요 없어요. 방법 3은 둘을 HTML에 같이 둘 수 없을 때만 써요. 포커스 관리가 서로 다르거나, 같은 id나 폼 필드가 두 번 들어가거나, 두 번 그리기에 무거울 때예요.
"아예 다른 디자인"으로 보여도, 두 시안에서 같은 요소에 같은 색을 칠해 보면 정말 다른 부분은 메뉴나 표 같은 한두 조각인 경우가 많아요. 그 조각에만 방법 2나 3을 쓰고 나머지는 그대로 둬요.
익숙해지는 방법
기본형 이름으로 부르기
레이아웃은 몇 가지 기본형의 조합이에요. 아래는 Every Layout이 붙인 이름 중 자주 쓰는 다섯 개예요.
해 보기이름마다 있는 부모 폭을 줄였다 넓혀 보세요. 그 이름의 모형들이 같이 바뀌어요. "HTML 코드 보기"를 펼쳐서 클래스를 누르면 그 클래스가 꺼지고 위 모형에 바로 반영돼요. 다시 누르면 켜져요. 눌러도 모형이 그대로일 때는 그 이유가 코드 아래에 나와요. Sidebar에서는 눌러서 달라진 것도 무엇이 왜 달라졌는지 나와요.
코드의 값은 모형과 같아요. 다만 모형은 lg: 대신 부모 폭 520px에서 바뀌고, 상자의 색과 안쪽 여백은 코드에서 뺐어요. Center의 점선은 px-4가 만드는 좌우 여백까지 포함한 상자예요.
Center mx-auto max-w-5xl px-4
└ Switcher grid gap-8 lg:grid-cols-2
├ Stack grid gap-4 ← 제목 · 설명 · 버튼
└ 카드
화면을 볼 때 이렇게 이름이 붙으면 묶음이 정해진 거예요. 이름이 안 붙으면 구조가 아직 애매한 거예요.
연습 방법
- 잘 만든 사이트를 개발자 도구로 열어요.폭을 줄였다 넓혔다 하면서 어느 요소에서 배치가 바뀌는지 찾아요. 그 요소가 묶음들의 부모예요.
- 코드를 쓰기 전에 시안 위에 상자를 그려요.종이든 Figma든 상관없어요. 의 질문에 답하는 과정이 눈에 남아요.
- 그린 상자를 기본형 이름으로 불러 봐요.몇 화면만 이렇게 해 보면 시안을 볼 때 상자가 먼저 보이기 시작해요.
3부배치 너머: 상호작용과 우선순위
여기까지는 요소를 어디에 놓느냐만 다뤘어요. 모바일과 데스크톱은 그 밖에도 달라요.
배치만 바꾸면 충분한가
화면 종류에 따라 달라요.
무엇이 같고 무엇이 다른가
내용과 기능은 줄이지 않아요. "모바일 사용자는 바쁘니까 기능을 빼자"는 접근은 권하지 않아요. 모바일로만 접속하는 사람에게는 뺀 기능이 곧 못 쓰는 기능이 돼요. 구글 검색도 모바일 화면의 내용을 기준으로 색인해요.
상호작용: 호버와 터치
데스크톱에서는 호버 효과가, 모바일에서는 터치 동작이 경험을 좋게 만들어요. 이걸 설계할 때 전제가 두 가지 있어요.
lg:로 나누지 않고 입력 장치 조건으로 나눠요.입력 장치 조건
| CSS 조건 | 뜻 | Tailwind v4 |
|---|---|---|
(hover: hover) | 호버가 되는 장치 | hover: (이 조건이 자동으로 붙어요) |
(pointer: fine) | 마우스처럼 정밀한 포인터 | pointer-fine: |
(pointer: coarse) | 손가락처럼 뭉툭한 포인터 | pointer-coarse: |
해 보기버튼에 마우스를 올리거나, 탭 키로 이동하거나, 눌러 보세요. 폰에서 열면 결과가 달라져요.
지금 이 기기 →
HTML 코드 보기
클래스를 누르면 꺼지고, 다시 누르면 켜져요. 눌러도 모형이 그대로면 그 이유가 코드 아래에 나와요.
호버를 터치로 옮기기
호버 효과를 설계할 때마다 "터치에서는 이 목적을 무엇으로 채우나"를 같이 정해요. 아래가 그 짝이에요.
| 호버로 하던 것 | 터치에서는 |
|---|---|
| 올리면 미리보기 | 스크롤로 화면에 들어올 때 재생, 또는 탭해서 열기 |
| 올리면 버튼이 나타남 | 항상 보이게 하거나, 옆으로 밀기, 길게 누르기 |
| 커서를 따라다니는 효과 | 스크롤에 연동된 움직임 |
| 툴팁 | 글자로 항상 표시하거나, 탭하면 열리는 말풍선 |
| 올리면 펼쳐지는 메뉴 | 탭하면 열리는 서랍·아코디언 |
| 화살표 눌러 넘기기 | 좌우로 밀기 |
잘 알려진 예시
| 서비스 | 마우스 | 터치 |
|---|---|---|
| YouTube | 썸네일에 올리면 미리보기 재생 | 스크롤하다 화면에 들어오면 자동 재생 |
| Netflix | 카드에 올리면 커지면서 예고편 표시 | 탭하면 상세 시트가 올라옴 |
| 그리드 사진에 올리면 좋아요 수 표시 | 두 번 탭해서 좋아요 | |
| 핀에 올리면 저장 버튼 표시 | 길게 누르면 메뉴가 펼쳐짐 | |
| Airbnb | 카드에 올리면 사진 넘김 화살표 표시 | 사진을 좌우로 밀어서 넘김 |
공통점은 같은 목적을 장치에 맞는 동작으로 바꿨다는 거예요. YouTube는 양쪽 다 "누르기 전에 미리 보기"가 목적이에요. 서비스 화면은 수시로 바뀌니 지금은 다를 수 있어요.
CSS만으로 되는 좌우 넘김
flex overflow-x-auto snap-x snap-mandatory와 자식의 snap-start면 돼요. 하단 시트나 밀어서 삭제처럼 손가락을 따라가는 동작은 보통 라이브러리를 써요.HTML 코드 보기
클래스를 누르면 꺼지고, 다시 누르면 켜져요. 눌러도 모형이 그대로면 그 이유가 코드 아래에 나와요.
lg:를 기준으로 데스크톱에만 호버 효과를 넣고 모바일은 비워 둬요. 모바일이 밋밋해지고 아이패드에서 어긋나요.focus-visible:을 같이 붙여요.우선순위와 이동: 일하는 화면
폰은 한 손으로 잠깐씩 쓰고 데스크톱은 책상에서 두 손으로 오래 써요. 그래서 조작을 반복하는 화면은 배치 말고도 아래가 달라져요.
| 달라지는 것 | 모바일 | 데스크톱 |
|---|---|---|
| 우선순위 | 한 화면에 한 가지 일. 나머지는 접어요. | 목록과 상세를 나란히 펼쳐요. |
| 이동 방식 | 엄지가 닿는 아래쪽 탭 | 위쪽이나 옆쪽 메뉴 |
| 입력 | 입력 칸을 줄이고 선택지나 자동 완성으로 | 타이핑, 단축키 |
| 장치 기능 | 카메라, 공유, 위치 | 끌어다 놓기, 여러 개 한꺼번에 처리 |
가장 흔한 예가 목록과 상세예요. 넓으면 둘을 나란히 보여 주고 좁으면 한 번에 하나만 보여 줘요.
해 보기넓은 상태에서 항목을 눌러 보고 부모 폭을 줄인 뒤 다시 눌러 보세요.
왼쪽에서 하나를 고르세요.
이런 차이는 의 방법 2나 3으로 만들어요. 위 데모는 방법 2예요.
모바일을 먼저 설계하라는 조언(Luke Wroblewski, 『Mobile First』)도 같은 맥락이에요. 좁은 화면에서 먼저 정하면 무엇이 중요한지 골라야 하고 데스크톱은 거기에 여유를 더하는 작업이 돼요. 반대로 하면 데스크톱 화면을 좁은 화면에 구겨 넣게 돼요.
마무리정리
읽은 순서와 실제로 작업하는 순서는 달라요. 화면 하나를 만들 때는 아래 순서로 해요.
한 화면을 만드는 순서
- 두 시안을 나란히 놓고 상자를 그려요.간격이 넓어지는 곳과 좌우로 갈라지는 곳이 경계예요. → ,
- 상자대로 HTML 구조를 써요.순서는 모바일에서 읽는 순서로 쓰고 배경과 폭 제한은 다른 요소에 둬요. →
- 묶음마다 flex인지 grid인지 골라요.폭을 서로 맞춰야 하면 grid, 내용 길이에 맡기면 flex예요. →
- 모바일 스타일을 쓰고, 달라지는 것만
lg:로 더해요.묶음 사이 간격은 부모의gap으로 줘요. → , - 상자가 안 맞는 조각만 따로 처리해요.CSS로 자리 옮기기, 숨기기, JS 순서로 시도해요. →
- 호버 효과를 넣을 때마다 터치에서의 짝을 정해요.호버로만 쓸 수 있는 기능은 만들지 않아요. →
- 일하는 화면이면 우선순위와 이동 방식을 따로 정해요.내용과 기능은 줄이지 않아요. → ,
더 읽을거리
| 이름 | 내용 |
|---|---|
| Every Layout | 레이아웃을 기본형으로 나누는 방법이에요. 의 이름이 여기서 왔어요. |
| Intrinsic Web Design | Jen Simmons가 붙인 이름이에요. 브레이크포인트를 늘리기보다 fr, minmax, auto-fit으로 스스로 맞춰지게 해요. |
| Atomic Design | Brad Frost의 방법이에요. 작은 부품 → 묶음 → 섹션 순으로 쌓아요. 컴포넌트를 어디서 자를지 정할 때 써요. |
| Mobile First | Luke Wroblewski의 책이에요. 좁은 화면에서 먼저 정해야 하는 이유를 다뤄요. |
| Tailwind 반응형 문서 | 모바일 우선 규칙과 컨테이너 쿼리(@container) 사용법이 있어요. |