본문 바로가기

frontend/CSS

[CSS] display: table 속성, 그리고 margin collapsing

이번 학기에 새로 들어간 프로젝트에서 프론트엔드 개발을 리액트로 진행하기로 결정이 나서, 어제부터 허겁지겁 리액트 공부를 시작했다.

리액트 공식 문서로 틱택토 게임 튜토리얼부터 진행하면서 리액트를 손에 익히는 중이다.
개강하고나서 웹 개발 쉰지도 꽤 돼서 사실 html, css, js 부분들도 한번씩 복습하면서 진행하면 좋겠다 싶었다. 그래서 리액트 외에 다른 부분에서도 공부 중에 새로 알게된 내용들도 기록하고자 한다 ❗

튜토리얼 진행 중에 display: table; 속성을 발견해서, 이와 관련해서 서칭을 좀 했다. 무엇보다 table 속성은 여태껏 거의 써본 기억이 없어서 나한테는 생소한 부분이였다. . .
table 속성은 어떨 때 어떻게 사용하는지, 그리고 이 속성에 대해 서칭하다가 새로 알게 된 margin collapsing 부분도 이번 글에서 정리하고자 한다.


display: table;


우선 이 table 속성은 여러 개의 컨텐츠(태그)들을 일정한 너비(width)로 나열하고자 할 때 주로 사용한다.

예를들어, 프로젝트의 진행 상황에 따라서 웹 페이지에 일정한 간격으로 나열하고자 하는 컨텐츠들의 수가 이전보다 증가하거나 감소하는 상황이 발생할 수 있다(기존에 있던 메뉴 리스트에서 새로운 메뉴 버튼을 추가시킨다거나 삭제시킨다거나 하는 경우).
만약 table 속성을 이용하지 않고, 각각의 컨텐츠를 태그에 담아서 일렬로 나열했다고 가정해보자.
그러면 컨텐츠의 개수가 변화할 때마다 각 컨텐츠 즉, 각 태그의 너비를 그때그때 수정해줘야 하는데, 매우 번거로운 작업임을 알 수 있다. 예를들어 부모태그(=컨텐츠들을 담을 컨테이너)의 너비를 기준으로, 컨텐츠의 개수가 두 개 일 때는 각 자식 태그의 너비를 50%로, 4개 일 때는 25%로 .. 이런식으로 컨텐츠 수에 따라 자식 태그의 너비값을 달리 부여해야 한다는 것이다.

하지만, display: table 을 이용하게 된다면 태그의 개수 변동에 따라 각 태그의 너비를 일일이 변경하는 작업을 거치지 않아도 된다. 즉, 이 속성을 부여한 태그(=display: table;) 내부에 있는 각각의 자식 태그들(=display: table-cell;)을, 일정한 너비로 배분하는 역할을 한다.

부모 태그에 display: table; 속성을 부여하고, 자식 태그에 display: table-cell; 속성을 부여하면 된다.

<style>
    #table {
        display: table;
        width: 100%;
        table-layout: fixed; /* 자식 태그들의 너비가 각각 다를 때, 너비를 서로 동일하게 유지 */
        border-collapse: collapse; /* border 끼리 서로 겹치지 않게 함 */
        
    }
    .table-cell {
        display: table-cell;
        overflow: hidden;
        border: 1px solid #000;
    }
</style>

<body>
<div id="table">
    <div class="table-cell">
        <p>1</p>
    </div>
    <div class="table-cell">
        <p>10000000000000</p>
    </div>
    <div class="table-cell">
        <p>3</p>
    </div>
    <div class="table-cell">
        <p>4</p>
    </div>
    <div class="table-cell">
        <p>5</p>
    </div>
</div>
</body>

위 코드를 실행하면 아래와 같은 결과가 나온다.

실행결과

보다시피 table-layout 값을 fixed로 부여했기 때문에, 내용물이 양 옆으로 오버된 두 번재 칼럼의 너비도 다른 칼럼과 동일하게 유지된 것을 볼 수 있다. 만약 여기에 fixed 속성과 collapse 속성을 제외시키면 다음과 같은 결과가 나온다.

실행결과 - fixed, collapse 제외

그리고 collapse 속성을 제외시켰기 때문에, 사진을 보면 테이블 내부에 있는 선들은 서로 중첩되면서 외부 테두리 선보다 비교적 두꺼워진 것을 알 수 있다.

margin collapsing


이 margin collapsing(마진 상쇄)은 top - bottom (수직방향)으로 발생하며, left - right (수평방향) 에서는 발생하지 않는다.
margin collapsing이란, CSS에서 두 개 이상의 block-level 요소들의 수직방향 마진 값이 모두 적용되지 않고, 단 하나의 요소의 마진 값으로만 적용되는 것을 의미한다. 이러한 margin collapsing이 발생하는 경우는 세 가지로 나뉘는데, 다음과 같다.

1. 인접한 sibling 블록 간에 수직방향 마진이 겹치는 경우

이 경우에는 크게 두 가지 경우로 나뉠 수 있다.

👉 1-1. 두 마진 값이 서로 다른 경우
둘 중 더 큰 값으로 적용하며, 더 작은 마진 값은 무시된다.

👉 1-2. 값이 서로 같은 경우
두 마진 값의 중복을 상쇄한다. 예를들어 두 마진 값이 20px로 동일한 경우, 위아래 간격은 40px이 아니라 20px로 적용된다.
다음과 같은 상황을 예로 들 수 있다.

<style>
    .child:first-child {
        margin-bottom: 20px;
    }
    .child:nth-child(2) {
        margin-top: 20px;
    }
</style>

<body>
<div id="parent">
    <div class="child">
        <p>
            first child
        </p>
    </div>
    <div class="child">
        <p>
            second child
        </p>
    </div>
</div>
</body>

원래 의도대로라면, 첫 번째 자식 태그와 두 번째 자식 태그 사이에는 40px 만큼의 여백이 생겨야하지만, 실제 브라우저 상에서는 두 간격이 겹쳐지면서 20px 만큼의 여백만 발생한 것을 볼 수 있다.

first child의 margin-bottom 영역
second-child의 margin-top 영역

위 사진으로 알 수 있다시피, 두 마진 값이 중첩되어 적용되었음을 알 수 있다.

2. 높이가 0인(=blank 상태의) 블록 요소의 수직 방향 마진이 겹치는 경우

높이가 0인 블록 요소란, 내부에 컨텐츠가 존재하지 않으며 height이나 padding, border 등으로 수직방향 크기가 지정되지 않은 블록 요소를 가리킨다. 즉, 높이가 존재하지 않는 요소이기 때문에 해당 요소의 상단 마진과 하단 마진 사이의 경계가 존재하지 않게 된다. 이 경우에도 위와 비슷한 규칙들을 따른다.

👉 2-1. 요소의 상단에 존재하는 마진 값과, 요소의 하단에 존재하는 마진 값이 서로 다른 경우
둘 중 더 큰 값으로 적용하며, 더 작은 마진 값은 무시된다.

👉 2-2. 값이 서로 같은 경우
두 마진 값의 중복을 상쇄한다.

3. 부모 블록의 상단 margin과 자식 블록(first-child)의 상단 margin이 겹치는 경우
부모 블록의 하단 margin과 자식 블록(last child)의 하단 margin이 겹치는 경우

부모 블록과 first-child 혹은 last child 사이에 인라인 콘텐츠가 존재하지 않거나, 부모 블록의 상단 혹은 하단에 padding값이나 border값을 부여하지 않을 때 부모 태그와 자식 태그간에 margin collapsing이 발생하게 된다. 이때 상쇄된 마진은 무조건 부모의 바깥 부분에 렌더링된다.

👉 3-1. 부모의 margin-top과 first-child의 margin-top이 겹치는 경우
둘 중 더 큰 값으로 적용하며, 더 작은 마진 값은 무시된다. 값이 서로 같다면 중복을 상쇄한다. 그리고 적용된 마진은 무조건 부모의 외부에 렌더링된다. (=부모 내부 즉, 자식에 바로 인접해서 마진이 렌더링되는 경우는 없다.)

👉 3-2. 부모의 margin-bottom과 first-child의 margin-bottom이 겹치는 경우
위와 동일하다.


margin collapsing을 없애는 방법에는
👉 요소를 float 시키기
👉 block level의 요소를 inline-block으로 변경시키기
등이 있다.


위 내용을 종합해서 추가적으로, 블록태그에 pseudo-elements를 적용하여 display: table 속성을 넣는 경우도 살펴보자면, 우선 크게 :before, :after 로 나뉠 수 있다. 각 역할은 다음과 같다.

👉 :before pseudo-element 에 display: table을 적용하는 경우
top-margin collapse를 방지하고자 할 때 주로 사용

👉 :after pseudo-element 에 display: table을 적용하는 경우
floats 속성을 clear할 때 사용

리액트 튜토리얼을 진행하다가 다음과 같은 CSS 코드를 볼 수 있었는데, 이 경우에는 square 클래스에 적용한 float 속성을 clear하기 위해 사용되었음을 알 수 있다.


index.js

import React from 'react';
import reactDom from 'react-dom';
import ReactDOM from 'react-dom';
import './index.css';

class Square extends React.Component {
    render() {
        return (
            <button className="square">
                {this.props.value}
            </button>
        );
    }
}

class Board extends React.Component {
    renderSquare(i) {
        return <Square value={i}/>;
    }

    render() {
        const status = 'Next player: X';

        return (
            <div>
                <div className="status">{status}</div>
                <div className="board-row">
                    {this.renderSquare(0)}
                    {this.renderSquare(1)}
                    {this.renderSquare(2)}
                </div>
                <div className="board-row">
                    {this.renderSquare(3)}
                    {this.renderSquare(4)}
                    {this.renderSquare(5)}
                </div>
                <div className="board-row">
                    {this.renderSquare(6)}
                    {this.renderSquare(7)}
                    {this.renderSquare(8)}
                </div>
            </div>
        );
    }
}

class Game extends React.Component {
    render() {
        return (
            <div className="game">
                <div className="game-board">
                    <Board/>
                </div>
                <div className="game-info">
                    <div>{/* status */}</div>
                    <ol>{/* TODO */}</ol>
                </div>
            </div>
        );
    }
}

// =========================================

ReactDOM.render(
    <Game/>,
    document.getElementById('root')
);


index.css

.board-row::after {
    clear: both;
    content: "";
    display: table;
}

/* ... */

.square {
    /* ... */
    float: left;
    /* ... */
}



참고 사이트


https://stackoverflow.com/questions/25699262/css-using-displaytable-with-before-pseudo-element/25701312
[ Stack Overflow - CSS, using display:table with before pseudo element ]
https://stackoverflow.com/questions/9519841/why-does-this-css-margin-top-style-not-work
[ Stack Overflow - Why does this CSS margin-top style not work? ]
https://www.w3.org/TR/CSS2/box.html#collapsing-margins
[ W3C - 8.3.1 Collapsing margins ]
https://velog.io/@raram2/CSS-%EB%A7%88%EC%A7%84-%EC%83%81%EC%87%84Margin-collapsing-%EC%9B%90%EB%A6%AC-%EC%99%84%EB%B2%BD-%EC%9D%B4%ED%95%B4
[ CSS 마진 상쇄(Margin-collapsing) 원리 완벽 이해 ]