배워서 남 주자

볼드, 이탤릭 등 스타일 변경 기능 구현 (1)

미래에서 온 개발자 2024. 10. 20. 22:01

프론트엔드의 꽃은 에디터 만들기가 아닐까 싶다. 회사 제품이 CAT(Computer-Assisted Translation) tool이다보니 리치 텍스트 에디터는 아니지만 점점 그렇게 되어가고 있는...? 중이다 ㅋㅋㅋ 지난 9월 스프린트의 주요 에픽이 바로 스타일 구현이었다. 요구사항은 다음과 같았다. 
 

- 볼드, 이탤릭, 밑줄, 위첨자, 아래첨자 총 다섯 가지 스타일 제공 
- 각각의 스타일을 중복으로 적용 가능해야 함 
- ctrl + b 등 단축키와 스타일 버튼 두 가지 형식으로 제공
- 스타일 변경에 대해서도 실행취소/재실행 기능을 확장해서 제공해야 함 
- 서버와 스타일 정보에 대한 규격을 정해 사용자가 입력한 텍스트와 함께 변경된 스타일 정보를 실시간으로 저장

 
 
이미 실시간 텍스트 편집에 대한 기능은 실행취소/재실행 및 서버와 실시간 데이터 연동이 구현되어 있는 상태였고, 여기에 스타일 변경 기능만 얹어주면 되는 거였다. (말은 참 쉽죠? ㅋㅋㅋ) contenteditable 속성을 사용하고 있기 때문에 에픽 구현에 들어가기 전에도 이미 ctrl + b, ctrl + i, ctrl + u 단축키를 입력하면 이미 브라우저 화면 상에서는 볼드, 이탤릭, 밑줄이 적용되었다. 크롬 브라우저 기준으로 사용자가 클릭 또는 드래그로 선택한 텍스트 앞뒤로 각각 <b>, <i>, <u> 태그가 붙는 것이 contenteditable의 자체 동작이었다. 
 
가장 관건이 되는 것은 역시 서버와의 데이터 연동이었다. 처음에는 위의 contenteditable에서 자체적으로 주는 html 태그를 그대로 서버에 저장하는 방식을 먼저 논의했다. 
 
기존 데이터 스키마는 다음과 같았다.
 

interface Segment {
  id: number;
  src_text: string;
  trg_text: string;
}

 
 
trg_text 필드에 plain text가 담겼다면, 이제는 아래와 같이 html 태그가 담긴 string을 주고 받자는 거였다. 
 
as-is : "아버지가 방에 들어가신다."
to-be : "<b>아버지</b>가 방에 들어가신다." 
 
하지만 이 방법에는 심각한 문제가 하나 있었는데, 서버에서 기존 텍스트 검색 기능과 상충하는 이슈였다. 텍스트 검색 기능이란 전체 텍스트 중에서 사용자가 '아버지'를 검색한다면 '아버지'라는 단어를 포함하고 있는 문장의 목록을 반환해주는 기능인데, 이 검색 기능에 당연하게도 trg_text 필드를 사용하고 있었다. 이 필드를 plain text가 아닌 html string을 사용하게 된다면 검색 인덱싱 문제가 발생할 수 밖에 없었다. 이를 우회하기 위해 plain text와 스타일 정보를 가진 text 두 가지 필드를 가지게 된다면 기존 세그먼트 분할/병합이나 실행취소/재실행 등 엮이는 문제가 너무 많아지는 상황이었다. 
 
html 태그를 포함한 string이 아닌, 마크다운 방식도 마찬가지로 trg_text가 아닌 마크다운 정보를 포함한 또다른 텍스트 필드를 만들어야 했으므로 고려 대상이 될 수 없었다. 최종적으로 다음과 같은 json 기반 양식으로 스타일링 정보를 저장하기로 정했다.
 

type StyleType = 'bold' | 'italic' | 'underline' | 'subscript' | 'superscript';

interface Segment {
  id: number;
  src_text: string;
  trg_text: string;
  trg_text_styles: {
    range: [number, number];
    styles: StyleType[];
  }[];
}

 
 
가령 위의 "아버지가 방에 들어가신다."의 예시는 다음과 같은 데이터 형태가 된다. 

{
  trg_text: "아버지가 방에 들어가신다.",
  trg_text_styles: [
    { range: [0, 3], styles: ['bold'] },
  ],
}

 
 
"아버지가 방에 들어가신다."처럼 볼드와 이탤릭 등 중복되는 스타일 정보에 대한 처리도 가능해진다.

{
  trg_text: "아버지가 방에 들어가신다.",
  trg_text_styles: [
    { range: [0, 3], styles: ['bold', 'italic'] },
  ],
}

 
 
이렇게 데이터 양식을 정하면 텍스트와 스타일이 명확히 분리되기 때문에 텍스트 검색 인덱싱 문제에서 자유로워질 수 있다. 이후에 이번 에픽의 요구사항인 다섯 가지 스타일 외에도 취소선 등 추가적인 스타일을 적용해야 한다는 요구사항이 생겨도 StyleType만 추가하면 되기 때문에 확장에도 유리한 구조이다. 
 
다만, 클라이언트에서 위의 스타일 정보를 가지고 스타일을 렌더링하고 서버에 데이터 저장 요청을 할 때 추가 로직이 필요하다. 특히 텍스트 변경 시 스타일이 적용된 범위를 재계산해야 하는 부분이 까다로워진다. 예를 들어 "좌우 정렬"이라는 볼드체가 적용된 텍스트가 있을 때 사용자가 '좌우'와 '정렬' 사이에 ' 및 가운데 '라는 텍스트를 삽입한다면 사용자는 아무런 추가 동작을 하지 않고도 "좌우 및 가운데 정렬"과 같이 추가로 입력한 텍스트에도 볼드체가 적용되기를 기대하기 때문이다. 
 
생각보다 포스팅 분량이 길어지고 있다. 😅 서버와 데이터 스키마를 정한 이후 클라이언트에서 까다로운 처리들을 실제로 어떻게 구현했는지에 대한 이야기는 다음 편에 이어서 작성해 보도록 하겠다. 
 
 
 
📚 참고 자료

원활한 콘텐츠 작성을 위한 에디터 개발기 - 오늘의집 블로그

오늘의집의 집들이, 노하우 에디터의 개발 과정을 소개합니다.

www.bucketplace.com