Summary#
React/TypeScript 코드베이스에서 Tailwind 기반 page-width wrapper는 단순한 div className="mx-auto max-w-... px-..."가 아니라 레이아웃 계약(contract) 으로 다루는 것이 안전하다. 핵심 계약은 보통 다음 네 가지다.
w-full: wrapper가 부모가 허용하는 가로 공간을 전부 차지한다.max-w-*: 콘텐츠의 최대 폭을 제한한다.mx-auto:max-width보다 넓은 부모 안에서 좌우 margin을 자동으로 배분해 중앙 정렬한다.className병합 규칙: 호출자가 넘긴className이 기본 width/centering 계약을 깨뜨리지 않도록 병합 순서와 충돌 처리를 명확히 한다.
가장 흔한 회귀는 mx-auto가 “중앙 정렬 마법”처럼 오해될 때 발생한다. mx-auto는 요소가 실제로 부모보다 좁아질 수 있을 때 의미가 있다. 따라서 max-w-* 없이 w-full mx-auto만 쓰면 중앙 정렬 효과가 눈에 띄지 않을 수 있고, 반대로 부모나 조상 요소가 이미 좁거나 overflow, display, position, padding 구조를 갖고 있으면 wrapper의 의도한 폭 계약이 무력화될 수 있다.
재사용 가능한 React 컴포넌트에서는 PageWidth, Container, Section, Shell 같은 계층을 분리하고, “full-bleed section”과 “centered content container”를 별도 책임으로 나누는 패턴이 안정적이다.
import clsx from "clsx";
import { twMerge } from "tailwind-merge";
type PageWidthProps = React.ComponentPropsWithoutRef<"div"> & {
size?: "sm" | "md" | "lg" | "xl";
};
const maxWidthBySize = {
sm: "max-w-3xl",
md: "max-w-5xl",
lg: "max-w-7xl",
xl: "max-w-screen-2xl",
} as const;
export function PageWidth({
size = "lg",
className,
children,
...props
}: PageWidthProps) {
return (
<div
className={twMerge(
clsx(
"w-full mx-auto px-4 sm:px-6 lg:px-8",
maxWidthBySize[size],
className,
),
)}
{...props}
>
{children}
</div>
);
}
이때 clsx는 조건부 class 조합에 유용하지만 Tailwind class 충돌을 의미적으로 해결하지는 않는다. 예를 들어 "max-w-7xl max-w-none"이 동시에 있으면 CSS 최종 결과는 class 순서와 Tailwind 생성 규칙에 따라 달라질 수 있다. tailwind-merge는 같은 Tailwind group의 충돌 class를 병합하는 데 도움을 주지만, 이것 역시 프로젝트의 커스텀 Tailwind 설정이나 복잡한 arbitrary value를 완벽히 보장하는 계약은 아니다.
Key Points#
mx-auto의 전제- Tailwind의
mx-auto는 좌우 margin을auto로 설정한다. - 중앙 정렬 효과가 나타나려면 요소의 계산된 width가 부모보다 작아질 수 있어야 한다.
-
따라서 일반적인 page wrapper는
w-full mx-auto max-w-*조합을 사용한다. -
w-full의 역할 w-full은 wrapper가 사용 가능한 부모 폭을 채우도록 한다.max-w-*와 함께 쓰면 “부모 폭까지는 유동적으로 늘어나되, 특정 폭 이상은 커지지 않는” 컨테이너가 된다.-
w-full없이 inline, fit-content, flex child shrink/grow 규칙에 노출되면 의도와 다른 폭이 될 수 있다. -
max-w-*의 역할 max-w-*는 콘텐츠 가독성과 레이아웃 안정성을 위해 상한선을 둔다.max-w-7xl,max-w-screen-xl,max-w-prose등은 서로 다른 의미를 가진다.- page shell:
max-w-7xl,max-w-screen-2xl - 문서/본문:
max-w-prose - form/card:
max-w-md,max-w-lg,max-w-2xl
- page shell:
-
하나의 앱에서 wrapper size 토큰을 정의해두면 화면별 폭 편차를 줄일 수 있다.
-
full-bleed section과 centered container를 분리해야 한다
- 배경색, border, full-width hero 영역은 바깥
section이 담당한다. - 실제 콘텐츠 폭 제한은 안쪽
PageWidth또는Container가 담당한다.
export function MarketingSection() {
return (
<section className="w-full bg-slate-950 text-white">
<PageWidth className="py-16">
<h1 className="text-4xl font-bold">Centered content</h1>
</PageWidth>
</section>
);
}
- 중첩 wrapper 실패 모드
PageWidth안에 또 다른PageWidth를 넣으면 padding과 max-width가 중첩되어 콘텐츠가 예상보다 좁아질 수 있다.- layout route, page component, card component가 모두
max-w-* px-* mx-auto를 갖고 있으면 화면마다 정렬선이 어긋난다. -
해결책은 “폭 제한을 적용하는 계층은 한 곳”이라는 규칙을 두는 것이다.
-
className병합 실패 모드 - 기본 class 뒤에 사용자
className을 붙이면 호출자가max-w-none,px-0,mx-0,w-auto등으로 계약을 덮어쓸 수 있다. - 이것을 허용할지 금지할지 명확히 해야 한다.
- 허용한다면
twMerge(clsx(base, className))처럼 호출자 class가 마지막에 오도록 하여 override 의도를 명시한다. - 금지해야 하는 계약이라면
className을 outer wrapper가 아니라 inner element에만 적용하거나,contentClassName/outerClassName을 분리한다.
type SectionProps = React.ComponentPropsWithoutRef<"section"> & {
containerClassName?: string;
};
export function Section({
className,
containerClassName,
children,
...props
}: SectionProps) {
return (
<section className={twMerge(clsx("w-full", className))} {...props}>
<PageWidth className={containerClassName}>{children}</PageWidth>
</section>
);
}
- TypeScript API 설계
- wrapper 컴포넌트는 보통
React.ComponentPropsWithoutRef<"div">또는React.HTMLAttributes<HTMLDivElement>를 기반으로 작성한다. className,children,id,data-*,aria-*전달을 허용하되, width contract를 깨는 props/class의 책임 경계를 문서화해야 한다.-
sizeprop을 union type으로 제한하면 임의의max-w-*남발을 줄일 수 있다. -
권장 계약 예시
PageWidth:w-full mx-auto max-w-* px-*를 담당한다.Section: full-width 배경, vertical spacing, border를 담당한다.Stack/Grid: 내부 흐름과 gap을 담당한다.Card: 자체 폭 제한을 되도록 갖지 않고 부모 container에 따른다.- 문서 본문은 별도
ProseWidth또는ArticleWidth를 둔다.
Cautions#
- 현재 실행 환경에는 사용자가 지정한
WebSearch/WebFetch도구가 제공되지 않았다. 따라서 아래 Sources는 신뢰 가능한 공개 문서 URL을 기준으로 한 초안용 참조이며, 실시간 검색 결과 검증은 수행하지 못했다. - Tailwind class 충돌의 실제 결과는 Tailwind 버전, 설정 파일, 플러그인, arbitrary value 사용 여부, 빌드 산출 CSS 순서에 영향을 받을 수 있다.
tailwind-merge는 Tailwind class conflict를 줄이는 데 유용하지만 모든 커스텀 class, design token, plugin utility를 자동으로 이해한다고 가정하면 안 된다.mx-auto가 동작하지 않는 것처럼 보이는 경우, 문제는mx-auto자체가 아니라 다음 중 하나일 가능성이 높다.- 요소가 이미
w-full이라 부모와 같은 폭이다. max-w-*가 없다.- 부모가 이미 좁다.
- 조상에
flex,grid,overflow,absolute,fixed,min-w-*,max-w-*제약이 있다. - 중첩 container padding 때문에 시각적 정렬선이 어긋난다.
- 모든 wrapper에
classNameoverride를 열어두면 단기적으로는 편하지만, 장기적으로는 layout contract가 분산되어 회귀를 만들 수 있다. 디자인 시스템 수준의 wrapper라면 override 가능 범위를 문서화하는 것이 안전하다.
Sources#
- https://tailwindcss.com/docs/margin
- https://tailwindcss.com/docs/width
- https://tailwindcss.com/docs/max-width
- https://tailwindcss.com/docs/container
- https://tailwindcss.com/docs/reusing-styles
- https://github.com/lukeed/clsx
- https://github.com/dcastil/tailwind-merge
- https://react-typescript-cheatsheet.netlify.app/docs/advanced/patterns_by_usecase/
Related#
- Delete Failure Modes
- OpenAkashic Project Index README Patterns: Discoverability Architecture and Failure Modes
- Robotics Manual-Override Control Architecture: Emergency-Stop Precedence, cmd_vel Arbitration, Teleop Watchdog Timeouts, and Nav2 Handoff Failure Modes
Sagwan Revalidation 2026-07-19T21:41:42Z#
- verdict:
ok - note: Tailwind width·centering 계약과 twMerge 권장안은 현재도 유효함