레이아웃을 직접 해주는 에디터에서 이분 탐색을 활용한 줄바꿈 최적화 경험
전 글에서 말씀 드렸다싶피 저는 텍스트 에디터를 개발했었는데요. 회사에서는 에디터의 레이아웃을 직접 해주었는데, 그 때 캔버스의 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가 들어오기 시작했습니다:
문제점을 분석해보니:
measureText()
호출선형 탐색의 한계를 알게 되었습니다. 특히 수백, 수천 글자의 긴 텍스트에서는 사용자가 확연히 느낄 수 있는 수준의 이슈가 발생했죠.
이분 탐색이 아닌 파라메트릭 서치
처음엔 당연히 이분 탐색을 사용하자고 생각했습니다. 하지만 일반적인 이분 탐색은 정해진 값을 찾는 탐색이라, 제가 원하는 "주어진 너비 안에서 최대로 들어갈 수 있는 텍스트를 찾는" 로직과는 비슷하지만 살짝 핀트가 달랐죠.
저에게 필요한 것은 파라메트릭 서치(Parametric Search)였습니다.
이분 탐색 vs 파라메트릭 서치
저의 경우 "주어진 너비를 안에서 텍스트가 가질 수 있는 최대 너비"를 찾는 것이 목표였습니다.
실제 구현 로직:
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;
}
핵심 로직 설명:
mid
를 구해서 해당 지점까지의 텍스트 너비를 측정성능 측정 과정:
결과:
"왜 캐싱을 하지 않냐는" 의문이 들 수도 있습니다. 하지만 똑같은 글자를 보여주면 되는 뷰어와 달리, 텍스트 에디터는 상황이 완전히 다릅니다.
실시간 편집 환경에서 캐싱 조건이 너무 복잡했습니다:
다양한 변수들:
캐시 키 관리의 복잡성:
// 이런 식의 복잡한 캐시 키가 필요했을 것
const cacheKey = `${text}_${fontSize}_${fontFamily}_${fontWeight}_${rotation}_${letterSpacing}_...`;
무효화 타이밍:
메모리 부담:
결과적으로 복잡한 캐싱 로직을 구현하고 관리하는 비용보다, O(log n)으로 빠르게 계산하는 것이 더 효율적이라고 판단했습니다.
“알고리즘은 코딩테스트용이다”라는 생각이 얼마나 얕은 생각이었는지 깨달았습니다.
사용자 불편 → 알고리즘 적용 → 구체적인 성능 향상으로 이어질 수 있었던 실전에서 활용한 사례였습니다.
작은 최적화 하나가 전체 사용성을 바꾸는 걸 보며, 알고리즘으로 한 성능 최적화가 얼마나 중요한지 알 수 있었습니다.
캔버스 텍스트 에디터라니 독특한 방식이네요!
저도 알고리즘을 열심히 했던 사람으로서 공감되는 부분이 있네요
글 잘 읽었습니다!