Frontend 08
Flexbox의 두 축: 주축과 교차축 마스터하기
display: flex를 쓰면 뭔가 정렬되지만 flex-direction, justify-content, align-items, gap이 각각 어떤 축을 다루는지 헷갈린다.
근거 · 교안 p25-p28
1장: Flexbox 활성화와 주축의 탄생: display: flex와 flex-direction
솔라의 코드 에디터 화면에는 회색 배경의 큼지막한 사각형 안에, 똑같은 모양의 파란색 상자 세 개가 힘없이 세로로 쌓여 있었다. 마치 중력을 거스르지 못하고 차례차례 떨어진 블록 같았다. 게시판의 글 목록을 만들고 싶은데, 제목, 작성자, 날짜를 담을 이 파란 상자들이 제멋대로 한 줄을 전부 차지하는 게 문제였다.
“분명 display: flex를 쓰면 된다고 했는데…”
솔라는 중얼거리며 부모 요소인 회색 사각형의 CSS 규칙에 한 줄을 추가했다.
.container {
/* ... 다른 스타일들 ... */
display: flex;
}
엔터 키를 누르자마자, 마법처럼 파란 상자들이 일제히 가로로 정렬되었다. 제멋대로 공간을 차지하던 상자들이 서로의 영역을 존중하듯 나란히 줄을 서는 모습에 솔라는 만족스러운 미소를 지었다.
“됐어! 역시 flex야.”
그때, 솔라의 화면을 들여다보던 언니 루나가 조용히 물었다.
“게시판 헤더라면 지금 모양이 괜찮은데, 만약 게시글 목록처럼 다시 세로로 쌓아야 한다면?”
“어… 그건…”
솔라는 잠시 말문이 막혔다. 분명 flex를 쓰기 전에는 세로였는데, 이제 와서 원래대로 돌아가고 싶지는 않았다. flex의 다른 기능을 써서 세로로 정렬하고 싶었다. 솔라는 잠시 고민하다 flex-direction이라는 속성을 떠올렸다.
“이걸로 방향을 바꿀 수 있을 거야. flex-direction: row.”
코드를 추가했지만 화면에는 아무 변화가 없었다. 파란 상자들은 여전히 얌전히 가로로 늘어서 있었다.
“어라? 왜 그대로지?”
“row가 기본값이거든.”
루나의 말에 솔라는 고개를 끄덕이며 값을 column으로 바꿨다.
.container {
display: flex;
flex-direction: column;
}
그러자 파란 상자들이 다시 flex를 적용하기 전처럼 세로로 착착 쌓였다. 하지만 이전과는 미묘하게 다른 느낌이었다. flex의 통제 아래 질서정연하게 정렬된 듯한 인상이었다.
“오, 된다! 그럼 row는 가로, column은 세로. 간단하네. Flexbox는 결국 이렇게 한 방향으로 요소를 배치하는 도구구나.”
솔라가 뿌듯해하며 결론을 내렸다. 바로 그 순간, 루나가 책상 위의 자를 집어 들어 솔라의 모니터 화면에 가로로 길게 가져다 댔다. 파란 상자들이 늘어선 길을 따라 자가 놓여 있었다.
“flex-direction이 row일 때, 아이템들은 이 자를 따라서 흐르고 있어. 마치 강물처럼.”
루나는 말을 이으며 자를 세로로 돌렸다. 이번에는 column으로 쌓인 파란 상자들의 흐름과 일치했다.
“column일 때는, 강물이 위에서 아래로 흐르는 것처럼 아이템들이 이 새로운 길을 따라 배치되지.”
“응. 가로 길, 세로 길. 그 말이잖아.”
솔라가 고개를 갸웃거리자 루나가 자를 내려놓고 다시 물었다.
“단순히 ‘길’이라고만 생각하면, 다음에 아이템들을 정렬할 때 헷갈릴 수 있어. flex-direction은 그냥 방향만 정하는 게 아니야. 앞으로 우리가 내릴 모든 정렬 명령의 ‘기준’이 되는 축을 선포하는 일에 가까워.”
“기준이 되는 축?”
“응. 이 아이템들이 흘러가는 중심 도로라고 생각해 봐. 우리는 이 도로를 **주축(main axis)**이라고 불러. flex-direction을 row로 설정하는 순간, ‘지금부터 주축은 수평 방향이다’라고 선언하는 거야. 그 선언에 따라 다른 모든 정렬 속성들이 ‘아, 우리는 수평 방향을 기준으로 움직여야 하는구나’하고 알아듣게 돼.”
솔라는 루나의 말을 곱씹으며 자신의 코드를 다시 보았다. display: flex는 아이템들을 Flexbox의 세계로 불러들이는 초대장이었고, flex-direction은 그 세계의 기본 규칙, 즉 어느 쪽을 주된 길로 삼을지 결정하는 선언문이었다.
“아…”
솔라의 입에서 작은 탄성이 터져 나왔다.
“그러니까 ‘한 방향으로 배치한다’는 말의 진짜 뜻은, 단순히 가로 아니면 세로, 둘 중 하나로만 놓을 수 있다는 뜻이 아니었구나. flex-direction으로 ‘주축’이라는 기준을 딱 하나 정하고, 일단은 그 축을 따라서만 아이템을 다룬다는 의미였어.”
가로 혹은 세로라는 단순한 방향 전환이 아니었다. 앞으로의 모든 움직임에 영향을 미치는 ‘주축’의 탄생. 솔라는 자신이 flex-direction을 너무 얕봤다는 사실을 깨달았다. 이것은 단순한 방향 지시등이 아니라, 전체 레이아웃의 판도를 결정하는 나침반이었다.
솔라는 다시 코드를 flex-direction: row로 되돌렸다. 이제 파란 상자들이 늘어선 가로 방향은 그냥 ‘가로’가 아니라, 모든 정렬의 기준이 될 ‘주축’으로 보였다.
“좋아. 이제 주축이 뭔지는 알겠어. 주축이라는 도로를 만들었으니까… 그럼 이 도로 위에서 차들을 어떻게 배치할지는 어떻게 정해? 전부 도로 시작점에 몰아넣을 수도 있고, 중앙에 모을 수도 있고, 아니면 일정한 간격으로 띄워놓을 수도 있잖아.”
2장: 주축을 따라 정렬하기: justify-content
솔라의 코드 에디터에서 커서가 깜박였다. flex-direction: row; 바로 아랫줄이었다. 화면 속 파란 상자들은 여전히 회색 컨테이너의 왼쪽 가장자리에 얌전히 붙어 있었다. 솔라는 이 상자들을 컨테이너의 정중앙으로 옮기고 싶었다. 이제 이 가로 방향이 단순한 ‘가로’가 아니라 ‘주축’이라는 것을 알게 된 이상, 정렬 명령도 주축과 관련이 있을 거라 짐작했다.
“주축이라는 길 위에서 차들을 어떻게 배치할지 정하는 방법…”
솔라는 지난번 언니와 나눴던 대화를 떠올리며 중얼거렸다. 그녀는 인터넷 검색창에 ‘flex-direction row 중앙 정렬’이라고 입력했다. 수많은 결과 속에서 justify-content라는 속성이 반복해서 눈에 띄었다. 이름부터 ‘내용물(content)을 맞춘다(justify)‘는 뜻이니, 이게 틀림없었다.
솔라는 다시 코드로 돌아와 속성을 입력했다.
.container {
display: flex;
flex-direction: row;
justify-content: center;
}
결과는 즉각적이었다. 왼쪽에 옹기종기 모여 있던 파란 상자들이 부드럽게 미끄러지더니, 회색 컨테이너의 수평 중앙에 자리를 잡았다.
“됐다! 역시 이거였어.”
솔라는 신이 나서 다른 값들도 시도해 봤다. flex-end를 입력하자 상자들은 오른쪽 끝으로 이동했고, space-between을 입력하자 양 끝에 하나씩 붙고 나머지 하나는 그 중간에 배치되어 서로 균일한 간격을 유지했다.
“재밌는데? justify-content는 주축 위에서 아이템들을 어떻게 분배할지 정하는 거구나. 시작점, 중앙, 끝점, 아니면 간격을 벌리는 것까지.”
솔라가 만족스럽게 고개를 끄덕였다. 그녀의 생각은 간단했다. flex-direction: row로 주축을 수평으로 만들었으니, justify-content는 수평 정렬을 담당하는 속성인 것이다.
그때, 솔라의 어깨너머로 화면을 보던 루나가 조용히 키보드 쪽을 가리켰다.
“그럼 flex-direction을 column으로 바꾸면, justify-content: center는 어떻게 동작할까?”
“어… column으로 바꾸면?”
솔라는 잠시 멈칫했다. flex-direction을 column으로 바꾸면 주축은 위에서 아래로 흐르는 세로 방향이 된다. 하지만 justify-content는 방금 확인했듯 수평 정렬을 다루는 속성이었다.
“주축이 세로로 바뀌니까… justify-content는 더 이상 작동하지 않거나, 아니면 그래도 어떻게든 수평으로 가운데 정렬을 하려고 하지 않을까?”
솔라는 확신 없이 대답하며 코드를 수정했다.
.container {
display: flex;
flex-direction: column;
justify-content: center;
}
엔터 키를 누르자, 솔라의 예상은 보기 좋게 빗나갔다. 파란 상자들은 세로로 쌓인 채, 회색 컨테이너의 ‘수직’ 중앙으로 이동했다. 수평 위치는 전혀 변하지 않은 채였다.
솔라는 잠시 화면을 멍하니 바라봤다. 자신이 justify-content를 완전히 오해하고 있었다는 사실을 깨달았다. 이것은 ‘수평 정렬 도구’가 아니었다.
“아…”
탄식이 터져 나왔다.
“이 속성은 ‘수평’이나 ‘수직’을 기준으로 움직이는 게 아니었어. 오직 ‘주축’의 방향만 따라가는 거였구나.”
루나는 말없이 고개를 끄덕였다.
솔라는 퍼즐 조각이 맞춰지는 듯한 기분을 느꼈다. flex-direction이 ‘주축은 이제부터 이쪽이야!’라고 선포하는 나침반이라면, justify-content는 그 나침반이 가리키는 방향을 따라서만 움직이는 충실한 병사였다. 주축이 가로일 때는 가로 방향으로 정렬하고, 주축이 세로로 바뀌자 망설임 없이 세로 방향으로 정렬 방식을 바꾼 것이다.
“그러니까 justify-content의 center는 ‘수평 중앙’이 아니라 ‘주축 방향의 중앙’이라는 뜻이었네. flex-start는 ‘왼쪽’이 아니라 ‘주축의 시작점’이고.”
솔라는 다시 코드를 flex-direction: row와 justify-content: center로 되돌렸다. 이제 파란 상자들은 다시 수평 중앙에 놓였다. 하지만 이전과는 완전히 다르게 보였다. 그저 ‘가운데 있는 상자’가 아니라, ‘수평 주축의 중앙에 정렬된 아이템’으로 보였다. flex-direction이라는 원인과 justify-content라는 결과 사이의 명확한 인과관계가 머릿속에 그려졌다.
주축을 따라 아이템을 자솔라재로 배치할 수 있는 강력한 제어 장치를 손에 넣은 기분이었다. 하지만 솔라는 곧 새로운 의문과 마주했다.
“좋아. 주축 방향으로는 마음대로 할 수 있겠어. 그런데… 주축에 직각인 방향은? 지금 이 상자들은 가로 중앙에는 있지만, 여전히 컨테이너 위쪽에 딱 붙어 있잖아. 이걸 아래로 내리거나 세로 방향의 중앙에 놓으려면 어떻게 해야 하지? 주축 말고 다른 축도 있는 건가?“
3장: 교차축 이해와 정렬: align-items
솔라의 화면 속 파란 상자들은 이제 justify-content 덕분에 회색 컨테이너의 수평 중앙에 가지런히 놓여 있었다. 하지만 솔라의 표정은 완전히 만족스럽지 않았다. 상자들은 가로 방향으로는 질서를 찾았지만, 여전히 컨테이너의 위쪽 천장에 머리를 박은 듯 바싹 붙어 있었다.
“주축 방향으로는 마음대로 움직일 수 있는데… 이 답답한 위쪽 공간은 어떡하지?”
문제를 더 명확하게 보기 위해, 솔라는 컨테이너에 높잇값을 주기로 했다.
.container {
display: flex;
flex-direction: row;
justify-content: center;
height: 300px; /* 추가 */
background-color: #f0f0f0; /* 회색 배경 */
}
회색 컨테이너가 아래로 길어지자, 파란 상자들 아래로 휑한 빈 공간이 드러났다. 위쪽에 옹졸하게 달라붙어 있는 모습이 한층 더 도드라졌다. 이 상자들을 저 넓은 공간의 세로 중앙으로 옮기고 싶었다.
“가로 정렬이 justify-content였으니까, 세로 정렬은 다른 속성이겠지.”
솔라는 ‘flex 세로 정렬’이라는 키워드로 검색을 시작했다. align-items라는 속성이 금세 눈에 들어왔다. 이름부터가 ‘아이템들을 정렬한다’는 의미를 강하게 풍겼다.
“이거네. justify-content가 주축을 담당하고, align-items가 나머지, 즉 세로 방향을 담당하는구나.”
솔라는 단순 명료한 결론에 도달하며 자신 있게 코드를 추가했다.
.container {
display: flex;
flex-direction: row;
justify-content: center;
height: 300px;
align-items: center; /* 추가 */
}
엔터 키를 누르자, 파란 상자들이 부드럽게 아래로 미끄러져 컨테이너의 정확한 세로 중앙에 멈췄다. 수평 중앙, 수직 중앙. 완벽한 정중앙이었다.
“됐다! justify-content는 수평, align-items는 수직. 이제 완벽하게 이해했어.”
솔라가 의기양양하게 말하는 순간, 옆에서 지켜보던 루나가 조용히 질문을 던졌다. 지난번과 똑같은 패턴이었다.
“정말? 그럼 flex-direction을 column으로 바꾸면 어떻게 될까? align-items: center는 그대로 ‘수직 중앙’ 정렬을 할까?”
솔라는 순간 멈칫했다. flex-direction이 column이 되면 주축이 세로로 바뀐다. justify-content는 주축을 따라가므로 세로 중앙 정렬을 맡게 될 것이다. 그럼 align-items는?
“음… align-items는 원래 수직 정렬 담당이니까… 주축이 세로로 바뀌어도 여전히 수직 정렬을 하려고 들지 않을까? 아니면 justify-content랑 역할이 겹치니까 그냥 아무것도 안 하려나?”
솔라의 머릿속이 복잡해졌다. 자신의 ‘수평=justify, 수직=align’이라는 공식에 예외가 생길 수 있다는 불안감이 들었다. 그녀는 직접 확인하기 위해 코드를 수정했다.
.container {
display: flex;
flex-direction: column; /* 변경 */
justify-content: center;
height: 300px;
align-items: center;
}
결과는 솔라의 모든 예상을 완전히 빗나갔다.
파란 상자들은 세로로 쌓인 채, 컨테이너의 수직 중앙으로 이동했다. 이건 justify-content: center가 주축(이제 세로가 된)을 따라 정렬한 결과이니 예상대로였다.
그런데, 상자들은 동시에 ‘수평’ 중앙에 와 있었다. 분명히 왼쪽 정렬이 기본이었는데, align-items: center가 그들을 가로 방향의 중앙으로 이끈 것이다.
“어? 잠깐만. align-items가… 왜 상자들을 가로로 움직였지? 방금 전까지 세로로 움직였잖아!”
솔라는 혼란에 빠졌다. align-items의 정체성이 갑자기 바뀐 것 같았다. 수직 정렬 전문가인 줄 알았는데, 상황이 바뀌자 수평 정렬을 하고 있었다.
그때 루나가 책상에서 자 두 개를 가져와 서로 직각으로 교차시켜 십자(+) 모양을 만들었다. 그리고는 모니터 화면의 flex-direction: row 상태인 레이아웃 위에 가로 자를 가져다 댔다.
“이게 우리가 정한 **주축(main axis)**이야. justify-content는 이 축 위에서만 움직이지.”
루나는 이어서 가로 자와 교차된 세로 자를 가리켰다.
“그리고 주축에 항상 직각으로 교차하는 이 축이 바로 **교차축(cross axis)**이야. align-items는 바로 이 교차축의 지배를 받아.”
루나는 말을 마치고, flex-direction: column 상태의 레이아웃으로 시선을 옮겼다. 이번에는 십자 모양의 자를 90도 회전시켜 세로 자가 주축이 되도록 했다.
“주축이 세로가 되면, 교차축은 자연스럽게 가로가 돼. 주축과 교차축은 항상 함께 움직이는 한 쌍이니까.”
“아…!”
솔라의 눈이 커졌다. 모든 조각이 제자리를 찾는 순간이었다.
align-items는 ‘수직 정렬’ 속성이 아니었다. ‘교차축 정렬’ 속성이었다. 그저 flex-direction의 기본값이 row여서 주축이 가로, 교차축이 세로였을 뿐이다. 그래서 마치 align-items가 수직 정렬 전용인 것처럼 보였던 것이다.
“flex-direction이 나침반으로 주축의 방향을 정하면, 교차축은 그 방향에 맞춰 자동으로 수직인 방향으로 설정되는 거였구나. 그리고 align-items는 그 교차축을 맹목적으로 따라가는 거였어. ‘가로’나 ‘세로’라는 절대적인 방향이 아니라.”
솔라는 다시 코드를 flex-direction: row로 되돌렸다. 이제 상자들은 다시 수직 중앙에 정렬되었다. 하지만 솔라에게는 더 이상 ‘세로 중앙’으로 보이지 않았다. ‘가로 방향 주축에 수직인 교차축의 중앙’으로 보였다. 두 개의 축이 만드는 좌표계가 머릿속에 선명하게 그려졌다.
주축과 교차축. Flexbox 레이아웃을 지배하는 두 개의 강력한 축을 모두 제어할 수 있게 된 솔라는 뿌듯함을 느꼈다. 이제 어떤 방향으로든 아이템들을 자유롭게 배치할 수 있었다.
“좋아, 그럼 이제 게시판 목록처럼… 각 항목들을 정렬하고 사이사이에 깔끔하게 간격을 좀 줘 볼까?”
솔라는 justify-content: space-between을 적용해 보았다. 아이템들이 양 끝으로 멋지게 퍼졌다. 하지만 아이템들 사이의 간격만 벌어질 뿐, 아이템과 컨테이너의 테두리 사이에는 공간이 없었다. 각 아이템에 일일이 margin을 주는 건 번거로워 보였다.
“흠, 이 아이템들 사이사이에, 그리고 양 끝에도 균일한 간격을 주는 더 세련된 방법은 없을까?“
4장: 깔끔한 간격 설정: gap
솔라의 화면 속 파란 상자들은 justify-content: space-between 명령에 따라 회색 컨테이너의 양쪽 끝에 하나씩 붙고, 나머지 하나는 그 정중앙에 자리 잡고 있었다. 아이템들 사이의 공간은 균일했지만, 양 끝에 붙어버린 상자들이 어딘가 답답해 보였다. 게시판 헤더라면, 전체적으로 여유 공간을 가지는 편이 더 보기 좋을 것 같았다.
“이럴 땐 margin을 쓰면 되려나?”
솔라는 개별 아이템에 직접 간격을 주는 방법을 떠올렸다. 각 파란 상자를 대표하는 .item 선택자에 margin 속성을 추가했다.
.item {
/* ... 다른 스타일들 ... */
margin: 0 10px; /* 좌우로 10px 간격 추가 */
}
.container {
display: flex;
flex-direction: row;
height: 300px;
align-items: center;
justify-content: space-between; /* 아이템 사이 간격 벌리기 */
}
결과는 썩 만족스럽지 않았다. 가운데 상자의 좌우로는 margin 덕분에 간격이 생겼지만, 양 끝 상자에도 불필요한 바깥쪽 margin이 생겨 컨테이너와 미세하게 떨어졌다. 게다가 space-between으로 만들어진 공간과 margin으로 만들어진 공간이 뒤섞여 간격이 일정하지 않게 보였다. 더 복잡한 margin 선택자(:first-child, :last-child)를 쓰면 해결할 수는 있겠지만, 아이템이 추가되거나 삭제될 때마다 수정해야 하는 번거로운 작업이었다.
“이건 아닌 것 같아. 유연하지가 않잖아. Flexbox답지 않아.”
솔라가 투덜거리며 margin 코드를 지웠다. 바로 그때, 옆에서 지켜보던 루나가 솔라가 작성하던 코드의 위치를 가리켰다. 솔라는 지금까지 flex-direction, justify-content, align-items 모두 부모 요소인 .container에 작성하고 있었다. 하지만 방금 margin은 자식 요소인 .item에 직접 적용하려 했다.
“모든 아이템의 배치와 정렬은 컨테이너가 지휘하고 있었지. 그런데 간격만 아이템 각자에게 맡기는 게 어색하지 않아?”
루나의 말에 솔라는 잠시 생각에 잠겼다. 주축을 정하는 것도, 주축과 교차축을 따라 정렬하는 것도 모두 컨테이너의 역할이었다. 그렇다면 아이템들 사이의 ‘간격’을 만드는 지휘봉 역시 컨테이너가 쥐고 있는 게 자연스러웠다.
“컨테이너가 간격까지 한번에…?”
솔라는 ‘flex container spacing’이라는 키워드로 검색을 시작했고, 이내 gap이라는 속성을 발견했다. 이름부터 ‘간격’이라는 뜻을 노골적으로 드러내고 있었다. 솔라는 반신반의하며 .container에 gap 속성을 추가했다. justify-content 속성은 일단 flex-start로 되돌려 놓았다.
.container {
display: flex;
flex-direction: row;
height: 300px;
align-items: center;
justify-content: flex-start; /* 일단 시작점으로 정렬 */
gap: 20px; /* 추가 */
}
엔터 키를 누르자, 놀라운 일이 벌어졌다. 왼쪽에 붙어 있던 파란 상자들 ‘사이’에만 정확히 20px의 간격이 생겨났다. 첫 번째 상자는 여전히 컨테이너 왼쪽에 붙어 있었고, 마지막 상자의 오른쪽에는 아무런 공간도 추가되지 않았다. 오직 아이템과 아이템 사이에만 균일한 ‘틈’이 생긴 것이다.
“와… 이거 엄청 깔끔한데? margin이랑은 차원이 다르잖아.”
솔라는 감탄했다. gap은 마치 똑똑한 조수가 아이템들 사이에만 정확한 크기의 투명 블록을 끼워 넣은 것 같았다.
“이것도 혹시…”
솔라는 이제 패턴을 알 것 같았다. 그녀는 flex-direction의 값을 column으로 바꾸었다.
.container {
display: flex;
flex-direction: column; /* 변경 */
height: 300px;
align-items: center;
justify-content: flex-start;
gap: 20px;
}
예상대로였다. 파란 상자들이 세로로 쌓이면서, 이번에는 위아래로 20px의 간격이 정확하게 벌어졌다.
솔라의 머릿속에서 흐릿했던 개념이 선명해졌다. gap은 단순히 가로 또는 세로 간격을 만드는 속성이 아니었다. justify-content가 주축을 따르고 align-items가 교차축을 따랐던 것처럼, gap 역시 Flexbox의 두 축을 모두 이해하고 있었다. 주축이 가로일 때는 아이템들의 열(column) 사이에 간격을, 주축이 세로일 때는 아이템들의 행(row) 사이에 간격을 만들어준 것이다.
“그럼 가로 세로 간격을 다르게 줄 수도 있어?”
솔라는 gap을 row-gap과 column-gap으로 나누어 쓸 수 있다는 것을 확인하고는, 테스트를 위해 아이템을 몇 개 더 추가하고 flex-wrap: wrap 속성을 넣어 여러 줄로 만들어 보았다.
.container {
/* ... */
flex-wrap: wrap; /* 아이템을 여러 줄로 표시 */
row-gap: 40px; /* 행 간격 */
column-gap: 10px; /* 열 간격 */
}
이제 파란 상자들은 위아래로는 40px, 좌우로는 10px의 간격을 가진 바둑판 모양으로 깔끔하게 정렬되었다. 단 세 줄의 코드로 말이다.
“gap은 그냥 간격을 주는 게 아니라, Flexbox가 만들어낸 행과 열의 구조를 정확히 파악하고 그 경계선에만 공간을 만드는 거였구나. 축의 개념과 완전히 연결되어 있었어.”
솔라는 자신이 처음에 gap을 단순한 margin의 대체재 정도로 생각했던 것이 얼마나 얕은 생각이었는지 깨달았다. gap은 Flexbox의 질서 체계 안에서 작동하는, 훨씬 지능적인 간격 설정 도구였다.
주축 결정자(flex-direction), 주축 정렬 제어(justify-content), 교차축 정렬 제어(align-items), 그리고 이제 Flex 항목 간격 설정(gap)까지. 솔라는 레이아웃을 자솔라재로 주무를 수 있는 강력한 도구들을 모두 손에 넣은 기분이었다.
“좋아. 이제 진짜 거의 다 아는 것 같아.”
솔라는 자신감에 찬 목소리로 말했다. 그녀는 다시 코드를 원래의 파란 상자 세 개로 되돌리고, 게시판 헤더처럼 보이도록 flex-direction: row와 적당한 gap을 설정했다.
“제목, 작성자, 날짜를 이렇게 나란히 배치하고… 좋아. 그럼 이 헤더 아래에, 세로로 긴 글 목록은 어떻게 붙이지? 지금까지 배운 것들을 합쳐서 진짜 게시판 모양을 만들려면 어떻게 시작해야 할까?“
5장: 게시판 레이아웃 만들기: Flexbox 종합 적용
솔라의 코드 에디터 화면에는 이제 제법 그럴듯한 게시판 헤더가 자리를 잡고 있었다. 회색 컨테이너 안에 파란색 상자 세 개가 ‘제목’, ‘작성자’, ‘날짜’처럼 적당한 간격(gap)을 유지하며 가로로 늘어서 있었다. 주축과 교차축의 개념을 이해하고 나니, flex-direction, justify-content, align-items, gap 같은 속성들이 마치 손에 익은 도구처럼 느껴졌다.
“좋아. 헤더는 완성. 그럼 이제 이 아래에 진짜 게시글 목록을 붙여야지.”
솔라는 자신감에 차서 HTML 코드를 수정하기 시작했다. 헤더를 구성하던 세 개의 <div> 요소 바로 아래에, 게시글 목록 역할을 할 새로운 <div> 다섯 개를 추가했다. 그녀의 머릿속 그림은 명확했다. 가로로 배치된 헤더 아래, 다섯 개의 게시글이 세로로 차곡차곡 쌓이는 것. 지금까지 배운 속성들을 조합하면 충분히 가능해 보였다.
<!-- 솔라's initial attempt -->
<div class="container">
<!-- Header items -->
<div>제목</div>
<div>작성자</div>
<div>날짜</div>
<!-- Post list items -->
<div>첫 번째 게시글입니다.</div>
<div>두 번째 게시글입니다.</div>
<div>세 번째 게시글입니다.</div>
<div>네 번째 게시글입니다.</div>
<div>다섯 번째 게시글입니다.</div>
</div>
하지만 화면에 나타난 결과는 솔라의 기대와는 전혀 달랐다. 새로 추가된 게시글 목록이 헤더 아래에 세로로 쌓이는 대신, 기존 헤더 아이템들 옆으로 꾸역꾸역 비집고 들어가 한 줄로 늘어서려고 했다. 컨테이너가 비좁아지자 결국 아래로 떨어지긴 했지만, 그건 의도된 세로 배치가 아니라 자리가 없어 밀려난 것에 불과했다. 모든 아이템이 하나의 거대한 가로 행에 속하려는 것처럼 보였다.
“어? 이게 아닌데. flex-direction은 row가 맞고… 그럼 새 아이템들은 왜 아래로 안 가지?”
솔라는 혼란에 빠졌다. 각 속성의 기능은 분명히 알고 있었다. flex-direction으로 주축을 정하고, justify-content와 align-items로 정렬하고, gap으로 간격을 준다. 그런데 왜 이 강력한 도구들이 한데 모이니 엉뚱하게 작동하는 걸까? 마치 훌륭한 연주자들을 모아놓았는데 각자 다른 악보를 보고 연주하는 듯한 불협화음이었다.
그때, 솔라의 막막한 표정을 본 루나가 조용히 다가와 화면을 들여다봤다. 루나는 코드의 문제를 지적하는 대신, 책상 위 포스트잇 한 장을 떼어내 솔라 앞에 놓았다. 그리고 그 위에 커다란 네모 하나를 그렸다.
“이게 우리 게시판 전체라고 생각해 봐.”
루나는 말을 이으며 큰 네모 안에, 위쪽에 길쭉한 가로 네모 하나와 그 아래에 세로로 더 큰 네모 하나를 그렸다. 위쪽 네모는 ‘헤더’, 아래쪽 네모는 ‘글 목록’ 영역이 되었다.
“지금 솔라 너는, 이 모든 아이템들을 하나의 커다란 상자(.container) 안에 넣고, 한 명의 지휘자에게 모든 걸 통제하라고 하고 있어. 지휘자는 flex-direction: row라는 지휘봉을 들고 있지. 그래서 모든 연주자(아이템)들에게 ‘전부 한 줄로 서세요!’라고 외치는 중이야.”
“아…”
솔라는 루나가 그린 그림과 자신의 코드를 번갈아 보았다. 그림 속에는 명백히 두 개의 다른 공간, 즉 헤더와 글 목록이 있었다. 헤더는 내부 아이템들이 가로로 움직여야 하고, 글 목록은 내부 아이템들이 세로로 쌓여야 했다. 완전히 다른 규칙이 필요한 두 개의 구역이었다.
“한 명의 지휘자가 아니라… 지휘자가 두 명, 아니 세 명이 필요했던 거구나.”
솔라의 머릿속에서 모든 것이 재구성되기 시작했다.
-
전체 판을 지휘할 총괄 지휘자: 게시판 전체(
board-container)를 위한 지휘자. 이 지휘자는 ‘헤더’와 ‘글 목록’이라는 두 개의 커다란 덩어리를 위에서 아래로 쌓아야 한다. 따라서flex-direction: column이라는 지휘봉을 들어야 한다. -
헤더 구역의 지휘자: 헤더(
.header-container)만을 위한 지휘자. 이 지휘자는 ‘제목’, ‘작성자’, ‘날짜’ 아이템들을 가로로 정렬해야 하므로flex-direction: row지휘봉을 든다. -
글 목록 구역의 지휘자: 글 목록(
.post-list-container)만을 위한 지휘자. 이 지휘자는 각 게시글을 세로로 쌓아야 하므로, 다시flex-direction: column지휘봉을 든다.
display: flex는 마법의 주문이 아니었다. 각자의 구역을 책임지는 지휘자를 임명하는 행위였다. 그리고 복잡한 레이아웃은 이렇게 여러 명의 지휘자가 각자의 구역을 책임지고, 그 지휘자들을 다시 더 큰 지휘자가 통솔하는 계층 구조로 만들어지는 것이었다.
“알겠어. 구조부터 다시 짜야 했어.”
솔라는 깨달음을 얻고 빠르게 코드를 수정했다. 먼저 HTML 구조를 루나의 그림처럼 명확하게 분리했다.
<!-- 솔라's revised structure -->
<div class="board-container">
<div class="header-container">
<div>제목</div>
<div>작성자</div>
<div>날짜</div>
</div>
<div class="post-list-container">
<div>첫 번째 게시글입니다.</div>
<div>두 번째 게시글입니다.</div>
<!-- ... more posts ... -->
</div>
</div>
그리고 각 컨테이너에 맞는 지휘봉, 즉 CSS 규칙을 부여했다.
.board-container {
display: flex;
flex-direction: column; /* 총괄 지휘자: 헤더와 목록을 세로로! */
gap: 20px;
}
.header-container {
display: flex;
flex-direction: row; /* 헤더 지휘자: 아이템을 가로로! */
justify-content: space-between;
align-items: center;
}
.post-list-container {
display: flex;
flex-direction: column; /* 글 목록 지휘자: 아이템을 세로로! */
gap: 10px;
}
결과는 완벽했다. 헤더는 위쪽에 가로로 멋지게 정렬되었고, 그 아래에는 게시글 목록이 깔끔하게 세로로 쌓였다. 모든 요소가 자신이 속한 컨테이너의 주축과 교차축 규칙에 따라 질서정연하게 움직였다. 개별 속성들이 따로 놀던 것이 아니라, 자신이 지휘를 받아야 할 지휘자를 찾지 못해 혼란스러워했던 것뿐이었다.
솔라는 자신이 처음 Flexbox에 대해 읽었던 문장을 떠올렸다. ‘Flexbox는 한 방향으로 요소를 배치하고 정렬하는 레이아웃 도구다.’ 이제 그 문장이 완전히 다르게 읽혔다. Flexbox가 단 하나의 방향만 다룰 수 있다는 뜻이 아니었다. 하나의 Flex 컨테이너가 하나의 주축(main axis)을 기준으로 명확한 ‘한 방향’의 질서를 만든다는 의미였다. 그리고 진짜 힘은, 이렇게 명확한 질서를 가진 컨테이너들을 원하는 만큼 중첩하여 복잡하고 다차원적인 구조를 설계할 수 있다는 데 있었다. 이것이 바로 ‘축 기반 레이아웃 설계’의 핵심이었다.
완성된 게시판 레이아웃을 보며 솔라가 만족스럽게 중얼거렸다.
“좋아. 이제 여기에 사이드바를 추가하고 싶으면… 이 board-container 안에 ‘사이드바’랑 ‘메인 콘텐츠’ 컨테이너 두 개를 넣고, board-container의 flex-direction을 row로 바꾸면 되겠네. 각자 자기만의 축을 가지고 있으니까.”
더 이상 루나에게 다음 단계를 묻지 않았다. 솔라는 이미 다음 구조를 머릿속에 그리고 있었다. Flexbox는 더 이상 혼란스러운 속성들의 집합이 아니라, 예측 가능하고 강력한 설계 도구가 되어 있었다.