알고리즘으로 프론트엔드 최적화 하기

colon·2025년 6월 14일
15

에디터 개발하기

목록 보기
2/5

레이아웃을 직접 해주는 에디터에서 이분 탐색을 활용한 줄바꿈 최적화 경험

왜 줄바꿈을 수동 처리 했을까?

전 글에서 말씀 드렸다싶피 저는 텍스트 에디터를 개발했었는데요. 회사에서는 에디터의 레이아웃을 직접 해주었는데, 그 때 캔버스의 API를 사용했습니다.

직접 레이아웃을 해주다 보니 글자의 줄바꿈도 수동으로 처리해야 했는데요. 특히나 텍스트 에디터는 사용자의 입력이나 반응(스타일 변경)에 민감하게 반응하다보니, 즉각적으로 줄바꿈을 처리해 주어야 합니다.

즉 모든 텍스트 렌더링시에 줄바꿈이 되는지 텍스트의 너비를 체크하는 계산이 필요했습니다.

이번에는 줄바꿈 로직을 최적화하는 과정에서, 프론트엔드 개발에서 알고리즘을 어떻게 활용했는지에 대해 공유해보려고 합니다.


처음엔 단순하게 시작했습니다

선형 탐색 방식

캔버스 API 활용해서 텍스트 너비 측정하기

활용한 캔버스의 API는 텍스트의 너비를 알 수 있는 canvas.measureText() 였습니다.

const metrics = ctx.measureText('안녕하세요');
console.log(metrics.width); // 예: 72.5 (픽셀 단위)

해당 API는 현재 설정된 폰트 스타일에 따라 텍스트가 실제로 차지할 픽셀 너비를 정확하게 계산해줍니다.

초기 선형 탐색 방식: 처음에는 입력된 텍스트를 기준으로 한 글자씩 선형으로 탐색 범위를 늘려가면서 너비를 체크했습니다.

// 기존 선형 탐색 방식 (의사코드)
function findLineBreakPoint(text, maxWidth) {
  for (let i = 1; i <= text.length; i++) {
    const partialText = text.substring(0, i);
    const width = canvas.measureText(partialText).width;
    
    if (width > maxWidth) {
      return i - 1; // 이전 인덱스가 최대 지점
    }
  }
  return text.length;
}

VOC가 들어오기 시작했습니다:

  • 긴 텍스트를 입력하는 사용자들의 지연 체감 증가
  • 긴 텍스트에 갑자기 스타일을 변경할 때 버벅거림 현상
  • 다양한 긴 텍스트 관련 성능 이슈가 지속적으로 접수

문제점을 분석해보니:

  • 텍스트가 길어질수록 계산 시간이 선형적으로 증가 (O(n))
  • 100글자 텍스트라면 최악의 경우 100번의 measureText() 호출
  • 실시간 편집 환경에서는 매 입력마다 이 계산이 반복

선형 탐색의 한계를 알게 되었습니다. 특히 수백, 수천 글자의 긴 텍스트에서는 사용자가 확연히 느낄 수 있는 수준의 이슈가 발생했죠.


성능 이슈, 그리고 파라메트릭 서치

이분 탐색이 아닌 파라메트릭 서치

처음엔 당연히 이분 탐색을 사용하자고 생각했습니다. 하지만 일반적인 이분 탐색은 정해진 값을 찾는 탐색이라, 제가 원하는 "주어진 너비 안에서 최대로 들어갈 수 있는 텍스트를 찾는" 로직과는 비슷하지만 살짝 핀트가 달랐죠.

저에게 필요한 것은 파라메트릭 서치(Parametric Search)였습니다.

이분 탐색 vs 파라메트릭 서치

  • 이분 탐색: "배열에서 값 X를 찾아라"
  • 파라메트릭 서치: "이분 탐색 기반으로 조건을 만족하는 최대값/최소값을 찾아라"

저의 경우 "주어진 너비를 안에서 텍스트가 가질 수 있는 최대 너비"를 찾는 것이 목표였습니다.

실제 구현 로직:

function findLineBreakPoint(text, maxWidth) {
  let left = 0;
  let right = text.length;
  let result = 0;
  
  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    const partialText = text.substring(0, mid);
    const width = canvas.measureText(partialText).width;
    
    if (width <= maxWidth) {
      result = mid;  // 현재까지 가능한 최대값 저장
      left = mid + 1;  // 더 긴 텍스트 시도
    } else {
      right = mid - 1;  // 더 짧은 텍스트 시도
    }
  }
  
  return result;
}

핵심 로직 설명:

  1. 중간점 계산: mid를 구해서 해당 지점까지의 텍스트 너비를 측정
  2. 조건 판단:
    • 너비가 허용 범위 내면 → 오른쪽 절반 탐색 (더 긴 텍스트 시도)
    • 너비가 초과하면 → 왼쪽 절반 탐색 (더 짧은 텍스트 시도)
  3. 최적값 저장: 조건을 만족하는 최대값을 지속적으로 업데이트

성능 측정 과정:

  • 측정 도구: 브라우저 개발자 도구의 Performance 탭 활용
  • 테스트 방법: 10초 동안 텍스트를 길게 누르며 연속 입력 측정
  • 비교 대상: 선형 탐색 vs 파라메트릭 서치 직접 비교
  • 측정 시점: 스타일 변경 시 응답 시간 체크

결과:

  • 성능 향상: 26.53ms → 13.41ms (50% 향상)
  • 시간복잡도: O(n) → O(log n)으로 개선
  • 체감 효과: 특히 긴 텍스트일수록 차이가 극명하게 드러남
  • 사용자 VOC: 기존 성능 관련 불편사항 해결 완료

캐싱이 해답일까? 직접 계산이 나았던 이유

"왜 캐싱을 하지 않냐는" 의문이 들 수도 있습니다. 하지만 똑같은 글자를 보여주면 되는 뷰어와 달리, 텍스트 에디터는 상황이 완전히 다릅니다.

실시간 편집 환경에서 캐싱 조건이 너무 복잡했습니다:

  1. 다양한 변수들:

    • 글자별로 다른 폰트 스타일 (굵게, 기울임, 크기 등)
    • 텍스트 회전 각도
    • 문자 간격, 줄 간격 설정
    • 언어별 렌더링 방식 차이
  2. 캐시 키 관리의 복잡성:

    // 이런 식의 복잡한 캐시 키가 필요했을 것
    const cacheKey = `${text}_${fontSize}_${fontFamily}_${fontWeight}_${rotation}_${letterSpacing}_...`;
  3. 무효화 타이밍:

    • 사용자가 텍스트를 수정할 때마다
    • 스타일을 변경할 때마다
    • 레이아웃이 바뀔 때마다
    • 모든 관련 캐시를 찾아서 삭제해야 함
  4. 메모리 부담:

    • 수많은 조합의 텍스트와 스타일 정보 저장
    • 캐시 크기 제한 및 LRU 관리 필요
    • 메모리 누수 위험성

결과적으로 복잡한 캐싱 로직을 구현하고 관리하는 비용보다, O(log n)으로 빠르게 계산하는 것이 더 효율적이라고 판단했습니다.


실무에서 알고리즘이 필요했던 순간

“알고리즘은 코딩테스트용이다”라는 생각이 얼마나 얕은 생각이었는지 깨달았습니다.

사용자 불편 → 알고리즘 적용 → 구체적인 성능 향상으로 이어질 수 있었던 실전에서 활용한 사례였습니다.

작은 최적화 하나가 전체 사용성을 바꾸는 걸 보며, 알고리즘으로 한 성능 최적화가 얼마나 중요한지 알 수 있었습니다.

profile
fe개발자

5개의 댓글

comment-user-thumbnail
2025년 6월 16일

캔버스 텍스트 에디터라니 독특한 방식이네요!
저도 알고리즘을 열심히 했던 사람으로서 공감되는 부분이 있네요
글 잘 읽었습니다!

1개의 답글
comment-user-thumbnail
2025년 6월 24일

저도 flitter.dev를 만들면서 여러 알고리즘을 썼는대, 알고리즘은 역시 알면 도움이 됩니다 ㅋㄷㅋㄷ

답글 달기