[마이 프로젝트] 블로그 목록형 바꾸는 법: 카드 두 줄 정렬

카드를 두 줄로 놓았을 때 읽는 순서가 뒤엉키는 문제를 나타낸 대표 이미지
눈은 가로로 읽는데 순서는 세로였습니다.

한 줄 요약: 가로로 넘기는 카드 목록을 두 줄로 만들었더니 1위 옆에 4위가 붙어 읽는 순서가 뒤엉켰습니다. 브라우저가 칸을 세로로 먼저 채우기 때문이었고, 화면에 그리기 전에 6개씩 묶어 순서를 다시 짜서 고쳤습니다. 이 글은 그 순서표와 코드, 그리고 순서를 바꿀 때 놓치기 쉬운 접근성 문제까지 정리합니다.

증상: 두 줄로 놓았더니 순위 순서가 뒤엉켰다

블로그 목록형 화면에서 카드를 두 줄로 늘어놓았더니 읽는 순서가 뒤엉켰습니다. 1위 다음에 2위가 와야 하는데 4위가 옆에 붙어 있었습니다. 순위가 중요한 목록이라서 이 어긋남이 특히 눈에 띄었습니다. 번호가 있는 목록에는 왼쪽에서 오른쪽으로, 윗줄에서 아랫줄로 읽는다는 암묵적인 약속이 있고, 이 약속이 깨지면 독자는 카드 내용을 읽기도 전에 목록이 잘못됐다고 느낍니다. 영화 목록처럼 번호가 곧 정보인 카드에서는 순서가 틀리면 목록 전체가 신뢰를 잃습니다.

이 글은 같은 문제를 겪는 분을 위해 원인, 해결 코드, 적용할 때 바꿔야 하는 숫자를 차례로 풀어 씁니다. 코드는 자바스크립트로 데이터 배열의 순서를 바꾸는 방식이며, 스킨의 HTML 구조를 따로 건드리지 않아도 됩니다. 마지막에는 CSS로 푸는 방법과의 비교, 그리고 이 방법이 가진 한계도 적었습니다.

읽고 나서 가져가실 것은 세 가지입니다. 원인을 한 줄로 설명하는 말, 그대로 옮겨 쓸 수 있는 함수, 그리고 내 화면에 맞추려면 어느 숫자를 바꿔야 하는지입니다. 어떤 화면에 이 방법이 맞고 어떤 화면에는 맞지 않는지의 판단 기준도 함께 적었습니다.

원인: 가로 스크롤에서 두 줄을 만들면 칸은 세로로 먼저 찬다

문제의 원인은 칸을 채우는 방향입니다. 가로로 넘기는 목록을 두 줄로 만들면, 브라우저는 위 칸을 다 채우고 아래로 내려가는 것이 아니라 세로로 먼저 채웁니다. 그래서 1위 아래에 2위가 오고, 그 옆에 3위가 옵니다. 보는 사람은 1위 옆에 2위가 있기를 기대합니다.

MDN 문서는 CSS 그리드의 grid-auto-flow가 항목을 어느 방향으로 채울지 정한다고 설명합니다. 값이 row이면 행을 하나씩 채우며 필요할 때 행을 더하고, column이면 열을 하나씩 채우며 필요할 때 열을 더합니다. 기본값은 row입니다. 가로로 계속 이어지는 두 줄 목록은 열을 더해 가며 채우는 구조여서 항목이 위아래로 먼저 쌓이는 것으로 볼 수 있습니다. 이 설명대로라면 문제는 코드의 오류가 아니라 채우는 방식의 성질입니다. MDN은 dense 값도 설명하는데, 앞쪽의 빈자리를 메우려고 뒤의 작은 항목을 끌어올려 항목이 소스 순서와 다르게 보일 수 있다고 합니다. 순위 목록에서는 이 값을 쓰지 않는 편이 안전하다는 뜻이 됩니다.

보이는 순서에 맞게 데이터를 미리 섞는다

CSS로 순서를 바꾸는 방법도 있지만, 데이터 순서를 아예 다시 짜는 쪽을 택했습니다. 화면에 그리기 전에 배열을 재배치합니다. 아래가 그 함수입니다.

function orderMoviesForTwoRows(items){
  items = items || [];
  if(isGpMovieMobileLayout()) return items;   // 폰은 한 줄이라 그대로
  var ordered = [];
  var pageSize = 6;                            // 한 화면에 3열 x 2행
  for(var start = 0; start < items.length; start += pageSize){
    var page = items.slice(start, start + pageSize);
    for(var col = 0; col < 3; col++){
      if(page[col])     ordered.push(page[col]);       // 윗줄
      if(page[col + 3]) ordered.push(page[col + 3]);   // 아랫줄
    }
  }
  return ordered;
}

6개씩 끊는 이유는 한 화면에 3열 2행이 보이기 때문입니다. 그 6개 안에서 1·4위, 2·5위, 3·6위 순으로 다시 세웁니다. 이러면 브라우저가 세로로 채워도 결과적으로 가로 순서가 맞습니다.

6개씩 묶어 열 단위로 순서를 다시 배열하는 구조
보이는 순서에 맞게 데이터를 미리 섞습니다.

6개 묶음의 순서표: 어떤 순위가 어디에 놓이나

코드가 하는 일을 표로 풀면 이해가 쉽습니다. 아래 표는 위 함수에 한 묶음 6개를 넣었을 때의 결과입니다. 배열을 새로 만든 순서와, 브라우저가 열마다 위에서 아래로 채운 뒤 화면에 보이는 위치를 나란히 적었습니다. 이 표는 함수의 코드와 앞서 설명한 채우기 방향을 따라 계산한 것입니다.

새 배열의 순서원래 순위화면 위치(열, 줄)
1번째1위1열, 윗줄
2번째4위1열, 아랫줄
3번째2위2열, 윗줄
4번째5위2열, 아랫줄
5번째3위3열, 윗줄
6번째6위3열, 아랫줄

윗줄을 왼쪽에서 오른쪽으로 읽으면 1, 2, 3위이고, 아랫줄은 4, 5, 6위입니다. 눈으로 읽는 순서와 순위가 일치합니다. 표를 읽는 요령은 간단합니다. 새 배열에서 홀수 번째는 윗줄, 짝수 번째는 아랫줄에 놓이고, 7번째 항목부터는 다음 묶음이 시작됩니다. 다만 새 배열의 순서는 1, 4, 2, 5, 3, 6이라는 점에 주의하세요. 이 차이가 뒤에서 다룰 접근성 문제의 원인입니다.

예시입니다. 이 표는 3열 2행일 때만 맞습니다. 만약 4열 2행 화면이라면 묶음 크기는 8개가 되고, 윗줄은 1~4위, 아랫줄은 5~8위가 되도록 반복 횟수와 더하는 숫자를 4로 바꿔야 합니다.

개수가 6의 배수가 아닐 때

영화 수가 6의 배수로 딱 떨어지지는 않습니다. 그래서 if(page[col])로 있는 것만 넣고, 없는 자리는 그냥 건너뜁니다. 처음에는 빈 칸을 채우려고 가짜 항목을 넣었는데, 회색 빈 카드가 보이는 쪽이 더 이상했습니다. 그냥 짧게 끝나는 것이 자연스러웠습니다. 있는 것만 넣는 검사를 빼면 비어 있는 자리를 처리하다 오류가 날 수 있으니, 이 한 줄은 지우지 않는 편이 좋습니다.

가상 예시입니다. 항목이 8개라면 첫 묶음 6개는 위 표대로 놓이고, 둘째 묶음에는 7위와 8위만 남습니다. 코드를 따라 계산하면 둘째 묶음은 7, 8 순으로 배열에 들어가고, 열마다 위에서 아래로 채우는 방식에서는 이 둘이 같은 열의 위아래에 놓일 수 있습니다. 마지막 묶음은 화면에서 한 번 눈으로 확인해 보시기 바랍니다.

확인 요령도 적어 둡니다. 개발자 도구에서 항목 수를 6개, 7개, 8개, 9개, 12개로 바꿔 가며 화면을 보면 됩니다. 6의 배수일 때는 위 표와 정확히 맞는지, 아닐 때는 마지막 열이 어떻게 놓이는지를 볼 수 있습니다. 어긋남이 눈에 띄면 마지막 묶음만 따로 처리하는 분기를 넣을지 판단하시면 됩니다.

두 줄 카드 정렬에서 정한 세 가지를 정리한 설명 이미지
화면 구조가 정해져야 순서를 짤 수 있습니다.

폰은 그대로 둔다

함수 맨 앞줄에서 폰이면 원래 순서를 그대로 돌려줍니다. 폰에서는 한 줄로만 보여주기 때문에 섞을 필요가 없습니다. 오히려 섞으면 순서가 망가집니다. 이 조건 하나가 빠지면 폰에서는 1, 4, 2, 5 순으로 한 줄에 나열되는 문제가 생깁니다. 한 줄로 보이는 화면에서는 소스 순서가 곧 화면 순서이기 때문입니다.

화면 구조에 따라 데이터 순서가 달라지는 코드라서, 둘을 같이 봐야 이해가 됩니다. 나중에 열 개수를 바꾸면 여기 숫자도 같이 고쳐야 합니다. 폰인지 판단하는 기준은 화면 스타일 쪽의 기준과 같아야 하므로, 한쪽만 바꾸지 않도록 같이 확인하세요. 적용하실 때는 pageSize와 반복문의 3, 그리고 page[col + 3]의 3을 자기 화면의 열 수와 맞추면 됩니다. 이 세 곳이 서로 묶여 있다는 점이 이 방법의 약한 고리입니다.

  • pageSize를 한 화면에 보이는 칸 수(열 수 x 줄 수)로 맞춥니다.
  • 바깥 반복문의 횟수와 page[col + 3]의 3을 열 수에 맞춥니다.
  • 폰 여부를 판단하는 함수가 실제 화면 폭 기준과 같은지 봅니다.

순서를 바꿀 때 놓치기 쉬운 접근성

MDN의 그리드와 접근성 문서는 그리드로 항목을 다시 배치하는 일은 시각적인 재배열일 뿐이라고 설명합니다. 화면 낭독이나 Tab 키 이동 순서는 HTML 소스 순서를 따르고, 그리드 배치는 비시각 매체의 순서를 바꾸지 않습니다. 문서는 순서 관련 속성을 시각적 재배열에만 쓰라고 안내하며, 소스 순서를 대신할 수 없다고 강조합니다.

W3C의 CSS 그리드 명세도 같은 취지로 적고 있습니다. 작성자는 order와 그리드 배치 속성을 내용의 논리적 순서가 아니라 시각적 재배열에만 써야 하며, 이 속성들은 음성 같은 비시각 매체의 순서나 키보드 이동의 기본 순서를 바꾸지 않는다고 합니다.

이 글의 방법은 반대로 데이터 순서 자체를 바꾸므로 소스 순서가 1, 4, 2, 5, 3, 6이 됩니다. 눈으로 보는 순서는 맞지만, 화면 낭독기나 키보드로 카드를 넘기는 사용자는 이 순서대로 만나게 됩니다. 순위 정보가 카드 안에 글자로 들어 있다면 큰 문제가 되지 않을 수 있습니다. 그래도 접근성이 중요한 화면이라면 이 점을 알고 선택하셔야 합니다.

선택을 돕기 위해 두 방법을 비교했습니다. CSS 방법은 직접 시도해 보지 않았으므로, 표의 해당 칸은 공식 문서의 설명만 옮겼습니다.

방법화면에서 보이는 순서Tab·낭독 순서적용할 때 조심할 점
데이터 순서를 다시 짜기(이 글)위아래 두 줄에서 순위가 가로로 맞음1, 4, 2, 5, 3, 6으로 소스 순서를 따름열·줄 숫자가 코드에 고정됨
CSS의 grid-auto-flow로 채우는 방향 지정row는 행 단위, column은 열 단위로 채움(MDN)소스 순서 그대로dense를 쓰면 DOM 순서와 다르게 보일 수 있음(MDN)

한계와 다른 방법

3열과 2행이 코드에 숫자로 박혀 있습니다. 화면 크기에 따라 열 개수가 달라지면 이 숫자도 계산해서 넣어야 하는데, 지금은 고정입니다. 가운데 크기 화면에서는 순서가 약간 어긋납니다. 또 위에서 다룬 것처럼 소스 순서가 순위 순서와 다릅니다.

CSS의 grid-auto-flow: column으로 푸는 방법도 있었을 것 같은데, 시도해 보지 않았습니다. 그래서 어느 쪽이 더 낫다고 말씀드릴 수 없습니다. 열 수가 자주 바뀌는 화면이라면 CSS 쪽을 먼저 검토하시고, 열 수가 고정이고 스킨 코드를 크게 바꾸기 어렵다면 이 글의 방법이 빠른 선택입니다. 정리하면 열 수가 고정이고 순위가 눈에 보이는 목록에는 이 글의 방법이, 화면 폭에 따라 열 수가 바뀌는 목록에는 CSS 쪽 검토가 먼저입니다. 어느 쪽이든 바꾼 뒤에는 Tab 키로 카드를 넘겨 보면서 이동 순서가 납득되는지 확인하는 습관을 권합니다. 수정 작업을 AI 도구에 맡길 때 작업 단위를 나누는 방법은 작업 모드를 써 보는 순서를 정리한 글에서, 사용 가능 범위를 먼저 따져 보는 순서는 누가 쓸 수 있고 어디서 시작하는지를 정리한 글에서 볼 수 있습니다.

확인 자료