CSS 말줄임표 뒤에 컨텐츠 두기

스포일러 - CSS Shape를 사용합니다

하루 일과를 마치고 컴퓨터를 끄려다가 질문을 하나 보게 되었다.

그러니까 질문자가 원한 건 다음과 같이 순수하게 CSS만 사용해서 말줄임도 되면서 말줄임이 되는 위치를 조금 앞으로 당겨서 여유가 생긴 공간에 (아마도) 버튼 같은 걸 넣고 싶다는 것이었다.

단계별로 비슷하게 구현해보자.

두 줄로 줄이고 말줄임표 표시하기

긴 텍스트를 두 줄로 줄이는 방법부터 살펴보자. 박스의 크기를 넘어가는 컨텐츠는 숨기도록 설정하고( overflow: hidden ), 텍스트가 넘어간다면 말줄임표를 표시하도록 하며( text-overflow: ellipsis ), 아직은 -webkit- 접두어가 필요한 몇 가지 속성을 더 사용해서 박스 안에 두 줄만 표현하도록 한다. 이와 관련한 내용은 어렵지 않게 찾을 수 있을 것이니 따로 설명하지 않고 코드부터 보겠다.

<div class="text">
  잔소리를 두루 늘어놓다가 남이 들을까봐
  손으로 입을 틀어막고는 그 속에서 깔깔댄다.
  별로 우스울 것도 없는데 날씨가 풀리더니
  이 놈의 계집애가 미쳤나 하고 의심하였다.
</div>

<style>
.text {
  display: -webkit-box;
  overflow: hidden;
  text-overflow: ellipsis;
  -webkit-line-clamp; 2;
  -webkit-box-orient: vertical;

  /* 아래 값은 편의를 위해 추가함. 임의로 변경 가능 */
  width: 200px;
  padding: 0 10px;
  border: 1px solid silver;
  font-size: 14px;
  line-height: 24px;
}
</style>

이 코드를 브라우저에서 확인해보면 다음과 같이 설정한 너비와 두 줄에 맞춰서 텍스트가 줄어든 것을 볼 수 있다. 원래 텍스트는 화면에 보이는 것보다 길다는 사실을 떠올리자.

말줄임표 뒤에 링크 버튼 추가하기

위 코드에서 링크 버튼을 추가해보자. <a> 태그를 사용해서 만든다고 할 때 이 태그는 다음 중 어디에 두어야 할까?

  1. 여는 <div> 바로 앞
  2. 여는 <div> 바로 뒤
  3. "틀어막고는 그" 바로 뒤
  4. 닫는 <div> 바로 앞
  5. 닫는 <div> 바로 뒤

잠깐 생각해보자.

만들기 나름이기는 하겠지만 줄임의 대상이 되는 텍스트보다 뒤에 있으면 링크 버튼 역시 줄임 또는 감춤의 대상이 되어버린다. 텍스트의 중간에 두는 것은 기능적으로 문제가 생길 가능성은 물론이고 접근성 측면에서도 해롭다. 버튼은 박스 안에 둘 것이므로 답은 1번 보다는 2번이 더 적합하다.

링크 버튼을 추가하고 스타일을 몇 가지 설정한 후 간단하게 떠올릴 수 있는 float: right 속성을 추가했다.

<div class="text">
  <a class="more">more?</a>
  잔소리를 두루 늘어놓다가 남이 들을까봐
  손으로 입을 틀어막고는 그 속에서 깔깔댄다.
  별로 우스울 것도 없는데 날씨가 풀리더니
  이 놈의 계집애가 미쳤나 하고 의심하였다.
</div>

<style>
.text {
  ...
}
.more {
  float: right;
  margin-top: 26px;
  height: 10px;
  line-height: 14px;
  padding: 3px 5px;
  background: #3c99dc;
  color: #fff;
}
</style>

.more의 스타일을 적절하게 설정해서 두 번째 줄에 어울리는 위치로 옮겼지만 결과를 보면 버튼 윗 공간에 있던 텍스트 역시 아래로 밀렸음을 알 수 있다.

이 때 사용할 수 있는 기능이 바로 CSS의 Shape이다. CSS Shape를 사용하면 그림 등을 따라 자연스럽게 어울리는 텍스트 형태를 만들 수 있다. MDN에서 CSS Shape를 검색하면 발견할 수 있는 다음 화면을 보면 어떤 의미인지 쉽게 이해될 것이다.

CSS Shape 예제

버튼의 모양은 박스 형태로 몹시 단순하기 때문에 사용해야 할 속성은 shape-outside 하나 뿐이며 속성의 값 또한 단순하게 border-box 면 충분하다. padding-box, content-box 등도 사용할 수 있는데 이에 관해서는 CSS의 박스 모델(box model)을 알고 있다면 어렵지 않게 이해할 것이라 생각한다. 뿐만 아니라 circle, ellipse, polygon, path 등을 사용해 복잡한 모양도 설정할 수 있다. 또한 CSS Shape는 모던 브라우저에서 잘 지원하는 기능이므로 특별한 처리없이도 사용할 수 있다.

이제 위에서 작성한 .more 클래스에 다음과 같은 속성 한 줄을 추가해주자.

shape-outside: border-box;

완성된 모양은 다음과 같다. 최종적으로는 버튼 부분이 말줄임표를 왼쪽으로 민 것 같은 형태가 된다.

완성된 결과물

최종 결과물과 코드는 JSBin 링크에서도 볼 수 있다.

다만 문제가 하나 있는데, Chrome을 제외한 다른 브라우저에서는 -webkit-box가 적용된 박스 내부에 있는 플로팅 요소를 제대로 표시하지 못한다. 아직 접두어가 필요한 확장 기능이라 불안정하게 지원되는 것인지도 모르겠다. 어쨌든 CSS Shape 자체는 여러 브라우저에서 잘 지원되는 기능이지만 앞서 말한대로 플로팅 요소를 표현함에 있어서 호환성 문제가 있다. 따라서 이 방법은 크롬 또는 크로미움 브라우저에서만 사용할 수 있는 방법으로 보인다.

이어지는 섹션은 글을 쓴 다음날인 10월 15일에 추가되었습니다.

유동성 지원

이 글을 공유한 후 트위터의 jude kim님으로부터 한 가지 피드백을 받았다.

피드백 내용: 마진 값을 26px 넣으셨는데요, 그렇게 되면 텍스트의 유동성을 보장할 수 없습니다. 글이 짧거나 긴 데이터를 고려해야 합니다.

어차피 크롬을 제외한 다른 브라우저에서는 -webkit-box 속성과 float 속성이 기대했던 대로 동작하지 않아서 보편적으로 적용 가능한 답은 안 나오겠지만, 적어도 크롬에서만은 언급된 문제를 해결하고 싶었다. 문제는 높이가 고정적이지 않고 유동적으로 변하기 때문에 자식 요소에서는 텍스트가 포함된 컨테이너의 높이에 비례해서 위치를 정할 수 없다는 점이었다.

이리저리 리서치하고 고민한 끝에 한 가지 눈에 띄는 사실을 발견했다. CSS Flexible Box Layout 명세에 이런 내용이 있다. (발번역 포함)

If the flex item has align-self: stretch, redo layout for its contents, treating this used size as its definite cross size so that percentage-sized children can be resolved.

플렉스 아이템에 align-self: stretch가 설정되어 있으면 컨텐츠에 관해 레이아웃을 다시 실행하고, 이 때 사용된 크기를 명시적인 교차 크기처럼 다루기 때문에 퍼센트 크기를 가진 자식도 처리한다.

CSS Flexible Box Layout - 9.4.11 Determine the used cross size of each flex item.

쉽게 말해서 컨텐츠의 높이가 유동적인 어떤 엘리먼트가 있다고 할 때 그 엘리먼트의 부모가 플렉스 박스이면, 해당 엘리먼트의 높이가 마치 height 속성으로 설정된 듯 사용되어 엘리먼트의 자식에서 퍼센트 높이를 사용할 수 있다는 뜻이다. 스펙을 봤으니 당연히 테스트를 해보아야 한다. 위에서 보여준 코드에 래퍼를 추가해보았다. 박스를 꾸미는 스타일도 래퍼로 옮겨왔다.

<div class="wrapper">
  <div class="text">
    ...
  </div>
</div>

<style>
.wrapper {
  display: flex;  
  width: 200px;
  border: 1px solid silver;
  padding: 2px 10px;
}
.text {
  ...
}
.more {
  ...
}
</style>

여기까지 실행하면 그냥 오른쪽이 전부 다 비어있는 형태가 된다. 아직 중간 과정이니까 실망하지는 말자.

그 다음에는 .more 버튼도 래퍼로 감싼다. 래퍼의 클래스는 .float으로 사용하고 다음과 같이 스타일을 업데이트한다.

<div class="wrapper">
  <div class="text">
    <span class="float"><a class="more">more</a>
    ...
  </div>
</div>

<style>
...
.float {
  float: right;
  display: flex;
  align-items: flex-end;
  height: 100%;
  shape-outside: inset(calc(100% - 24px) 0 0 0);
}
.more {
  line-height: 14px;
  padding: 3px 5px;
  background: #3c99dc;
  color: #fff;  
  margin-bottom: 3px;
}
</style>

이제 결과를 실행해보면 다음과 같이 보일 것이다. 줄 수를 변화시켜 보아도 깔끔하게 잘 정리가 되는 편이다.

원리는 아까 설명한 대로 래퍼에 display: flex를 설정하면 자식 노드, 그러니까 .textalign-self: stretch가 설정되어 있으면 높이가 유동적이라 해도 다시 레이아웃을 하면서 자신의 크기를 확정된 크기처럼 다룬다. 덕분에 .text의 자식 노드인 .float은 마치 .text의 높이가 설정되어 있는 것처럼 퍼센트 높이를 사용할 수 있게 된다. 여기서는 height: 100%을 주어 .text 오른쪽의 높이를 가득 채우도록 했다.

이번에는 shape-outside 속성에 padding-box 대신 inset을 사용했는데, 이 속성은 텍스트가 플로팅된 엘리먼트의 영역 일부를 넘어올 수 있게 허용해준다. inset에 전달된 네 개 숫자 중 첫 번째는 상단의 시작 지점을 알려준다(MDN 문서 참고). 여기서 사용한 calc(100% - 24px) 값은 바닥 라인에서 위로 24px만큼 떨어진 위치를 의미한다. 텍스트의 줄 높이가 24px로 설정되어 있는 것을 생각하면 텍스트 마지막 줄의 상단이 될 것이다. 사실 이 값은 1px부터 텍스트의 줄 높이인 24px 사이의 어느 값을 설정해도 같은 결과가 나타난다.

.float 내부에 있는 .more의 위치는 .floatalign-items: flex-end 속성을 적용해 아래쪽에 두었다. 그 결과 버튼은 하단에 위치하게 된다. 이제 컨텐츠가 많거나 적어도, 줄 수가 달라져도 유동적으로 버튼의 위치를 설정해주는 코드가 완성되었다. 앞서 설명했던 내용을 반영한 후, 직접 줄 수를 변경할 수 있도록 간단한 UI를 추가해두었다. 최종-다시-최종 결과물은 https://jsbin.com/bebuwuhibe/1에서 볼 수 있다.

완성된 진짜 최종 결과물

  1. 평소 퍼블리싱 관련으로 해당 부분 구현할 수 있는 방법 없는지 한참을 찾아보고 고민했었는데 본 것 중에 가장 명확하고 좋은 코드인 것 같아요 글 잘 읽었습니다 감사드려요 !

댓글을 남겨주세요

This site uses Akismet to reduce spam. Learn how your comment data is processed.