긴 텍스트가 박스를 벗어날 때 어떻게 처리할지는 웹 개발에서 자주 만나는 문제다. 카드 레이아웃, 리스트 아이템, 댓글 목록처럼 고정된 너비에 텍스트를 맞춰야 할 때가 많기 때문이다. 다만 많은 개발자들은 CSS의 text-overflow나 JavaScript의 substring()을 막무가내로 섞어서 쓰거나, 불필요한 라이브러리를 끌어오곤 한다. 이번에는 한 줄 자르기부터 여러 줄 제한까지 상황별로 최적의 방법이 뭔지, 그리고 언제 JavaScript가 필요하고 언제 CSS만으로 충분한지 정확히 구분해서 설명하겠다.

 

1단계. CSS만으로 충분한 경우와 JavaScript가 필요한 경우

텍스트 자르기는 크게 두 가지 상황으로 나뉜다. 첫째, 단순히 시각적으로 텍스트를 숨기는 경우. CSS의 text-overflow와 overflow로 말줄임표를 보여주면 된다. 둘째, 실제 텍스트 길이 제한이 필요한 경우. 서버로 보내는 데이터나 DOM 조작 전에 미리 문자열을 자르려면 JavaScript가 필요하다. 예를 들어 사용자 입력값을 데이터베이스에 저장하기 전에 길이를 제한해야 할 때는 JavaScript의 substring()이나 slice()로 처리해야 한다.

 

CSS text-overflow로 한 줄 자르기

가장 간단한 경우부터 시작하자. 텍스트가 한 줄을 넘어가면 말줄임표(...)를 붙이고 싶을 때는 CSS 세 줄만 추가하면 된다.

/* ✓ 올바른 방법 */
.truncate-single {
  width: 300px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
<p class="truncate-single">이것은 매우 긴 텍스트인데 한 줄을 넘어가므로 말줄임표가 붙을 것이다</p>

결과: "이것은 매우 긴 텍스트인데 한 줄을..."

핵심은 이 세 가지 조건을 모두 만족해야 한다는 점이다. overflow:hidden으로 넘친 내용을 숨기고, text-overflow:ellipsis로 말줄임표를 표시하고, white-space:nowrap으로 줄바꿈을 막아야 한다. 하나라도 빠지면 작동하지 않는다.

 

CSS line-clamp로 여러 줄 제한하기

반면 2줄 이상 제한해야 할 때는 text-overflow로는 불가능하다. 이때 쓰는 것이 line-clamp 속성이다. 모던 브라우저(2020년 이후)에서는 모두 지원한다.

/* ✓ 올바른 방법 */
.truncate-multi {
  width: 300px;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
<p class="truncate-multi">이것은 매우 긴 텍스트다. 첫 번째 줄, 두 번째 줄, 세 번째 줄까지만 보이고 나머지는 말줄임표로 생략된다.</p>

결과: 최대 3줄까지만 표시되고 나머지는 "..."로 생략됨

주의할 점은 -webkit- 프리픽스가 붙어 있다는 것이다. 이건 표준이 정해지기 전에 WebKit 브라우저(Chrome, Safari, Edge)에서 먼저 구현했기 때문이다. 표준 line-clamp 속성만 써서는 아직 모든 브라우저에서 작동하지 않으니, -webkit- 프리픽스를 함께 써야 한다.

 

2단계. JavaScript로 문자열 길이 제한하기

CSS로는 시각적으로만 자를 수 있다. 실제로 DOM에는 여전히 전체 텍스트가 남아 있다. 이게 문제가 되는 경우는 여럿이다. 예를 들어 검색 엔진이 전체 텍스트를 크롤링하거나, 사용자가 텍스트를 복사할 때 전체가 복사되거나, 또는 서버로 보내기 전에 미리 길이를 제한해야 할 때다. 이런 상황에선 JavaScript로 실제 문자열을 자르는 게 맞다.

/* ✗ 잘못된 방법 */
const text = "이것은 매우 긴 텍스트인데 한 줄을 넘어갈 수 있다";
const truncated = text.substring(0, 10); // "이것은 매우 긴"
// 문제: 정확히 몇 글자인지 사용자 눈높이에서 결정하기 어렵다

/* ✓ 올바른 방법 */
function truncateText(text, maxLength) {
  if (text.length > maxLength) {
    return text.substring(0, maxLength) + "...";
  }
  return text;
}

const result = truncateText("이것은 매우 긴 텍스트인데 한 줄을 넘어갈 수 있다", 15);
console.log(result); // "이것은 매우 긴 텍스트인..."

substring()은 인덱스로 자르므로 한글 같은 멀티바이트 문자에서도 정확하게 작동한다. 단순히 글자 수를 세는 것이기 때문이다. 반면 substr()은 두 번째 인자가 길이를 의미하므로 혼동하기 쉽다. 현재는 substr()이 deprecated 되었으니 substring()을 쓰는 게 정석이다.

 

공백이나 줄바꿈 처리까지 함께하기

실무에서는 텍스트에 여러 줄의 공백이나 탭 문자가 섞여 있을 수 있다. 단순히 자르는 것보다는 정리하면서 자르는 게 낫다.

function truncateAndClean(text, maxLength) {
  // 연속된 공백과 줄바꿈을 하나의 공백으로 정리
  const cleaned = text.replace(/\s+/g, " ").trim();
  
  if (cleaned.length > maxLength) {
    return cleaned.substring(0, maxLength) + "...";
  }
  return cleaned;
}

const messyText = "이것은  매우    긴\n\n텍스트입니다  ";
const result = truncateAndClean(messyText, 15);
console.log(result); // "이것은 매우 긴 텍스트입니다"

const longText = "이것은 매우 긴 텍스트인데 한 줄을 넘어갈 수 있다 정말 많이";
const result2 = truncateAndClean(longText, 15);
console.log(result2); // "이것은 매우 긴 텍스트인데 한..."

정규표현식 \s+는 공백, 탭, 줄바꿈을 모두 의미한다. 이를 공백 하나로 replace하면 깔끔하게 정리된다. trim()으로 앞뒤 공백도 제거하니 일석이조다.

 

3단계. 실전 패턴: 반응형 길이 자르기

화면 크기에 따라 자르는 길이를 다르게 해야 할 때가 있다. 모바일에서는 20글자, 데스크톱에서는 50글자처럼 말이다. 이때는 JavaScript와 미디어 쿼리를 함께 써야 한다.

function getMaxLength() {
  if (window.matchMedia("(max-width: 768px)").matches) {
    return 20; // 모바일
  }
  return 50; // 데스크톱
}

function renderTruncatedText(element, fullText) {
  const maxLength = getMaxLength();
  element.textContent = truncateText(fullText, maxLength);
}

const textElement = document.querySelector(".comment-text");
const fullComment = "이것은 사용자가 남긴 긴 댓글입니다. 화면 크기에 따라 길이가 자동으로 조정됩니다.";

renderTruncatedText(textElement, fullComment);

// 화면 크기 변경 시 다시 렌더링
window.addEventListener("resize", () => {
  renderTruncatedText(textElement, fullComment);
});

window.matchMedia()로 미디어 쿼리 조건을 JavaScript에서 확인할 수 있다. 사용자가 창을 리사이징하면 resize 이벤트가 발생하니, 이때 다시 계산해서 자르면 된다. 다만 resize 이벤트는 매우 자주 발생하므로, 실무에서는 debounce를 적용해서 성능을 최적화하는 게 좋다.

 

4단계. 흔한 실수와 주의사항

 

✗ 잘못된 것: CSS만 쓰고 실제 길이 검증 없이 서버로 보내기

프론트엔드에서 CSS로 시각적으로 자른다고 해서 서버가 안전해지는 건 아니다. 사용자가 브라우저 개발자도구를 열어서 CSS를 비활성화하거나, API를 직접 호출해서 제한 없이 긴 텍스트를 보낼 수 있기 때문이다. 반드시 백엔드에서도 길이 검증을 해야 한다.

/* ✗ 위험한 코드 */
const formData = new FormData();
formData.append("comment", document.querySelector(".comment").textContent);
// CSS로만 자르고 실제 검증은 안 함

fetch("/api/comment", { method: "POST", body: formData });

/* ✓ 안전한 코드 */
const comment = document.querySelector(".comment").textContent;
const MAX_LENGTH = 500;

if (comment.length > MAX_LENGTH) {
  alert("댓글은 " + MAX_LENGTH + "글자 이내여야 합니다.");
  return;
}

const formData = new FormData();
formData.append("comment", comment);

fetch("/api/comment", { method: "POST", body: formData });

 

✗ 잘못된 것: emoji나 특수문자 길이 계산 무시

이모지(😀)는 JavaScript에서 1글자처럼 보이지만, 실제로는 2바이트 이상의 유니코드 문자다. substring()으로 자를 때 이를 무시하면 이모지가 깨질 수 있다.

/* ✗ 잘못된 코드 */
const text = "안녕하세요😀 이것은 테스트입니다";
const truncated = text.substring(0, 8);
console.log(truncated); // "안녕하세요😀" (이모지가 불완전하게 잘림)

/* ✓ 올바른 코드 */
function safeSubstring(text, maxLength) {
  // 유니코드 시퀀스를 정확히 계산
  const codePoints = Array.from(text);
  return codePoints.slice(0, maxLength).join("");
}

const text = "안녕하세요😀 이것은 테스트입니다";
const truncated = safeSubstring(text, 8);
console.log(truncated); // "안녕하세요😀" (완전하게 자림)

Array.from()으로 문자열을 배열로 변환하면 각 그래프 클러스터(이모지 포함)를 정확히 분리할 수 있다. 그 후 slice()로 자르면 불완전한 이모지를 피할 수 있다.

 

✗ 잘못된 것: line-clamp로 높이 기반 자르기 기대하기

line-clamp는 줄의 개수 기준이지, 높이(px) 기준이 아니다. 글꼴 크기가 변하면 줄의 높이도 변하므로, CSS만으로는 정확한 높이 제어가 불가능하다. 높이 기반으로 자르려면 JavaScript로 실제 높이를 측정해야 한다.

/* ✓ 높이 기반 텍스트 자르기 */
function truncateByHeight(element, maxHeight) {
  const originalText = element.textContent;
  let text = originalText;
  
  while (element.scrollHeight > maxHeight && text.length > 0) {
    text = text.substring(0, text.length - 1);
    element.textContent = text + "...";
  }
}

const box = document.querySelector(".text-box");
truncateByHeight(box, 100); // 최대 높이 100px

 

5단계. CSS vs JavaScript 선택 기준

 

상황 추천 방법 이유
시각적으로만 자르기 (한 줄) CSS text-overflow 성능 최적, 유지보수 간단
여러 줄 시각적 제한 CSS line-clamp 반응형 처리 자동, 성능 우수
실제 문자열 길이 제한 (입력값) JavaScript substring 서버 전송 전 검증 가능
화면 크기별 다른 길이 JavaScript + matchMedia 반응형 길이 조정 가능
높이 기반 정확한 자르기 JavaScript scrollHeight 측정 픽셀 단위 제어 가능

 

마무리

텍스트 자르기는 단순해 보이지만, 실제 상황에 따라 CSS와 JavaScript를 올바르게 조합해야 한다. 시각적 자르기만 필요하면 CSS text-overflow와 line-clamp로 충분하고, 실제 데이터 길이를 제한해야 하면 JavaScript substring()을 쓰고, 반응형이거나 높이 기반이어야 하면 둘을 섞어야 한다. 작은 최적화가 모여서 사용자 경험과 성능을 크게 향상시킨다는 점을 잊지 말자. 이 글의 각 패턴을 참고해서 자신의 프로젝트에 맞는 방법을 선택하면, 깔끔하고 유지보수하기 좋은 텍스트 처리 로직을 구축할 수 있을 것이다.