빌드노트
첫 자식의 margin-top을 늘렸는데 부모 전체가 내려간다
부모 margin이 0이어도 첫 자식의 위쪽 margin은 부모 밖에서 합쳐질 수 있다. 실제 좌표와 화면으로 경계를 확인하고, 카드 내부 여백은 padding으로 정할지 자식 margin을 flow-root 안에 유지할지 선택한다.
카드 안의 첫 제목에 margin-top: 40px을 줬는데, 카드 배경 안쪽은 그대로이고 카드 전체가 내려갈 수 있다. 부모와 첫 자식의 위쪽 margin이 하나로 합쳐지는 margin collapse(마진 상쇄) 조건을 확인해야 한다. 이때 자식의 margin은 부모 안쪽 여백으로 남지 않는다.
카드 배경 안에 일정한 여백을 두려면 부모의 padding-top으로 옮기고 자식의 위쪽 margin을 없앤다. 자식의 margin을 유지하면서 부모 경계 밖으로 합쳐지지 않게 하려면 부모의 display: flow-root를 검토한다. 두 수정은 같은 화면을 만들 수 있지만 여백을 정하는 주체가 다르다.
부모의 margin이 0이어도 생긴다
CSS 2.1의 상쇄 규칙 (새 탭에서 열림)은 같은 블록 서식 맥락에서 일반 흐름에 참여하는 블록들의 맞닿은 위아래 margin을 다룬다. 부모와 첫 번째 흐름 안의 블록 자식 사이에도 적용된다. 위쪽 border·padding이나 줄 상자가 둘을 가르지 않고 자식에 float 회피용 clearance도 없다면, 위쪽 margin이 합쳐질 수 있다. 부모의 margin이 0이라는 이유로 이 경계가 분리되지는 않는다.
먼저 개발자 도구의 Elements에서 부모와 첫 자식을 찾는다. 제목 앞에 실제 텍스트 줄이 있는지, 부모의 padding-top과 border-top-width가 0인지 확인한다. 제목의 margin은 직접 쓴 CSS뿐 아니라 브라우저 기본 스타일에서 올 수도 있으므로 Computed 값을 읽는다.
다음 조회는 부모와 자식의 위쪽 좌표가 함께 움직이는지 확인한다. 문제가 보이는 문서의 Console에서 #card와 #heading을 자기 요소의 선택자로 바꿔 실행한다. 자식은 부모의 직접 자식이어야 한다.
(() => {
const p = document.querySelector('#card');
const c = document.querySelector('#heading');
if (!p || !c || c.parentElement !== p) throw new Error('부모와 직접 자식 선택자를 확인하세요.');
const pr = p.getBoundingClientRect(), cr = c.getBoundingClientRect();
return {
parentTop: pr.top, childTop: cr.top,
insideGap: cr.top - pr.top,
parentMargin: getComputedStyle(p).marginTop,
childMargin: getComputedStyle(c).marginTop
};
})()
아래 합성 예제에서 조회하면 parentTop: 140, childTop: 140, insideGap: 0이었다. 계산된 margin은 부모 0px, 자식 40px로 남아 있었다. 자식의 CSS가 사라진 것이 아니라, 40px이 부모 위쪽에서 배치에 반영된 것이다.
getBoundingClientRect 설명 (새 탭에서 열림)에 따르면 좌표는 뷰포트 기준이고 상자 크기에는 padding과 border가 포함된다. margin 영역을 포함한 좌표는 아니다. 실제 화면에서는 두 top의 차이가 0이라는 사실만으로 상쇄를 확정하지 말고, DOM과 계산된 스타일을 함께 대조한다. transform 등으로 옮긴 상자도 별도로 확인한다.
같은 40px인데 배경이 시작하는 위치가 달랐다
실제 서비스 화면과 분리한 새 HTML에서 비교했다. 위에 높이 100px의 회색 상자를 놓고, 그 아래 파란 부모와 주황색 첫 자식을 뒀다. 부모는 display: block, height: auto, min-height: 0이며 margin·padding·border는 모두 0이다. 자식 높이는 48px, 위쪽 margin은 40px이다. 둘 다 일반 흐름에 있고 앞에 텍스트 줄이나 float는 없다.
직접 비교하려면 다음 내용을 새 margin.html로 저장해 데스크톱 브라우저에서 연다. 설치할 라이브러리는 없다. 위 조회도 이 문서에서 그대로 실행할 수 있다.
<!doctype html>
<meta charset="utf-8">
<style>
html { writing-mode: horizontal-tb; }
body { margin: 0; }
#before { height: 100px; background: #e2e8f0; }
#card {
display: block; width: 320px; height: auto; min-height: 0;
margin: 0; padding: 0; border: 0; background: #93c5fd;
}
#heading {
height: 48px; margin: 40px 20px 0; background: #f59e0b;
}
</style>
<div id="before"></div>
<div id="card"><div id="heading">첫 자식</div></div>
Windows 11의 headless Chrome에서 얻은 좌표와 높이는 다음과 같았다. 좌표·길이 단위는 CSS px이고 페이지는 스크롤하지 않았다.
- 자식의 위쪽 margin을 0으로 바꾸면 부모 top 100, 자식 top 100, 부모 높이 48이었다.
- 원래의 자식 margin 40에서는 부모 top 140, 자식 top 140, 부모 높이 48이었다. 회색 상자 아래 40px은 흰색으로 보였다.
- 부모 padding-top 40, 자식 margin-top 0에서는 부모 top 100, 자식 top 140, 부모 높이 88이었다. 회색 바로 아래부터 파란 배경이 보였다.
- 부모 padding 0, display flow-root, 자식 margin-top 40에서도 부모 top 100, 자식 top 140, 부모 높이 88이었다. 위쪽 40px이 파란 배경 안에 보였다.
자식 margin만 80px로 늘리자 부모와 자식 top은 모두 180이 됐고 부모 높이는 여전히 48이었다. 첫 자식과 부모의 시작점이 함께 내려가는 증상이 좌표에서도 드러났다. 세 조건을 나란히 띄운 화면에서도 기본 조건의 흰 틈과 padding·flow-root 조건의 파란 내부 여백을 확인했다.
내부 여백은 padding에, 자식의 margin은 경계 안에
카드 자체에 위쪽 내부 여백 40px이 필요한 경우에는 앞 HTML의 CSS 뒤에 다음 규칙을 추가한다. 자식의 위쪽 margin을 0으로 바꾸는 것까지 한 수정이다.
#card { padding-top: 40px; }
#heading { margin-top: 0; }
자식 margin 40px을 남긴 채 부모 padding 40px만 더한 조건도 실행했다. 부모 top은 100, 자식 top은 180으로 내부 간격이 80px이 됐다. padding이 경계를 가른 뒤에는 두 길이가 모두 반영되므로, 기존 margin을 남길 의도인지 확인해야 한다.
반대로 자식이 자기 위쪽 margin을 유지해야 한다면 다음 규칙으로 비교한다. 앞의 padding 수정을 되돌리는 값도 함께 적었다.
#card { padding-top: 0; display: flow-root; }
#heading { margin-top: 40px; }
MDN의 display 설명 (새 탭에서 열림)에 따르면 flow-root는 새 블록 서식 맥락을 만든다. 부모와 자식의 상쇄 설명 (새 탭에서 열림)은 이 경계가 부모와 자식의 margin 상쇄를 막는다고 설명한다. 이 예제에서는 자식의 40px margin이 부모 내부에 남았다. 같은 조건에서 자식 margin을 0으로 바꾸면 내부 간격도 0이었다. flow-root 자체가 일정한 여백을 추가하는 속성은 아니다.
카드와 앞 상자 사이의 간격이 목적이면 부모에 margin을 둔다. 예제에서 부모 margin-top: 40px, 자식 margin-top: 0으로 바꾸면 부모와 자식 top이 모두 140이었고 내부 간격은 0이었다. 실제 코드에서 앞 상자의 아래쪽 margin도 있으면 인접한 margin의 상쇄 조건을 함께 확인한다. 양수 margin들이 상쇄될 때는 합계가 아닌 가장 큰 값이 쓰인다는 것이 CSS 2.1의 규칙이다.
높이 지정은 위쪽 경계를 가르지 않았다
기본 조건에서 부모의 height만 120px로 바꿔도 부모와 자식 top은 모두 140이었다. 부모 높이만 120으로 달라졌다. MDN의 조건 구분 (새 탭에서 열림)에서 높이·최소 높이는 마지막 자식과 부모의 아래쪽 상쇄를 판단할 때 나온다. 첫 자식의 위쪽 문제에 그 조건을 그대로 적용하면 원인을 놓친다.
상쇄를 끊는 작은 경계도 확인했다. 자식 margin 40을 유지한 채 부모에 padding-top: 1px 또는 border-top: 1px solid를 주면 부모 top은 100, 자식 top은 141이었다. 둘 다 경계가 생겼지만 내부 길이는 41px이 됐다. 필요한 여백을 표현하려면 원하는 padding 값을 정하고 자식 margin을 조정하는 편이 분명하다.
직접 실행한 범위는 양수 margin을 가진 일반 블록의 부모·첫 자식이다. 브라우저 user agent는 HeadlessChrome/153.0.0.0, 뷰포트는 1200×420, 장치 픽셀 비율은 1이었다. Firefox·Safari, 세로 쓰기, 음수 margin, 여러 조상에 걸친 상쇄와 실제 앱의 중첩 flex/grid는 실행하지 않았다. 문서상 flex/grid 컨테이너와 그 직접 아이템 사이에는 이 부모·자식 상쇄가 일어나지 않는다. 아이템 안의 일반 블록 관계는 따로 확인해야 한다. 부모가 이미 flex/grid라면 실제 경계를 먼저 확인하고, flow-root로 바꿀 때 자식 배치 방식까지 바뀐다는 점을 고려한다.