안녕하세요, SmartEditor 안드로이드 팀 소속 김지현입니다.
저는 이제 막 입사 한 달 차를 맞이한 병아리 개발자🐥 인데요, 정식으로 팀에 합류하기 전에 3개월간의 인턴 생활을 먼저 거쳤습니다. 이번 글에서는 해당 인턴 기간에 개발한 Code Syntax Highlighter의 개발 과정에 대해 이야기하고자 합니다.
Code Syntax Highlighter in SmartEditor


현재 Web SmartEditor에서는 이미 소스 코드 컴포넌트를 제공하고 있습니다. 하지만 Android SDK에서는 아직 제공하지 않는 기능입니다. 따라서 Android Native Code Syntax Highlighter를 개발하는 것이 이번 과제의 목표였습니다.
Web SmartEditor의 소스 코드 컴포넌트는 언어 판별 및 실시간 하이라이팅 기능을 지원하고 있지 않습니다. 따라서 기존 소스 코드 컴포넌트의 스펙을 그대로 따라가는 대신, 다음과 같은 새로운 기능을 추가하였습니다.
- 1. 다양한 언어를 자동 판별 및 지원하고, 서비스 측에서 새로운 언어를 주입할 수 있습니다.
- 2. 에디터 형태로 실시간 Syntax 하이라이팅을 지원합니다.
따라서 위 기능들을 포함한 Code Syntax Highlighter를 어떤 과정을 통해 구현했는지 살펴보도록 하겠습니다.
(*이하 Code Syntax Highlighting의 전체 진행 과정을 ‘하이라이팅‘으로 표현하겠습니다.)
기본 구조 및 논리
Code Syntax Highlighter를 구현하기 위해 Tokenizer, Lexer, (Color) Highlighter가 필요하다고 생각했습니다. 각각의 역할은 다음과 같이 정의할 수 있습니다.
- Tokenizer: input을 식별 가능한 단위(Token)로 쪼갬
- Lexer: 각 Token에 문맥적 의미를 부여
- (Color) Highlighter: 문맥적 의미를 기반으로 Token에 컬러를 입힘
따라서 위 세 가지 객체를 구현해 Code Syntax Highlighter를 구성하였습니다. 이를 도식화한 모습은 다음과 같습니다.

이렇게 Code Syntax Highlighter의 기본 구조 및 논리에 대해 살펴보았는데요, 이제 세부 기능 구현 과정을 좀 더 자세히 살펴보겠습니다.
기능1: 언어 판별 및 주입
다양한 언어를 지원하기 위해선 (1) 언어 정의 규칙과 (2) 언어 판별 로직이 필요합니다. 이를 어떻게 구현하였는지 순서대로 알아보겠습니다.
언어 정의 규칙

언어별 Syntax 정보를 정의하는 틀로 위와 같은 LanguageRegexSet
data class를 사용하였습니다. 예약어, 구두점, 연산자, 주석, 문자열 보간에 대한 문법을 정규 표현식 및 문자열 형태로 정의하는 방식입니다.
(*물론 위의 코드로 모든 언어의 Syntax를 완벽히 커버하는 것은 불가능합니다. 인턴이라는 제한된 기간 동안 러프하게 구현한 결과물로서, Syntax 정의 틀의 간단한 예시 정도로만 참고하시길 바랍니다. 더 정교한 Syntax 정의 방식이 궁금하시다면 해당 링크 참조를 추천드립니다.)

실제로 제가 작성 및 활용했던 Kotlin Syntax 정의 예시는 위와 같습니다.
언어 판별 로직

제가 구현한 Syntax Highlighter는 입력된 코드의 언어를 자동 판별할 수 있습니다. 따라서 언어 판별만을 담당하는 LanguageIdentifier 객체를 구현하였습니다. 이때, 언어 판별은 다음 규칙을 기반으로 진행됩니다.
- 언어별 keyword와 입력된 코드의 Token들을 매칭해, keyword가 가장 많이 매칭되는 언어를 반환합니다.
- 단, 함수명 혹은 변수명은 특정 언어의 keyword와 매칭이 되더라도 keyword 개수로 세지 않습니다.
(*정확도가 100% 보장되는 언어 판별 방식은 아닙니다.)
자동 언어 판별이 진행되는 모습은 위와 같습니다. 기본적으로 C, Swift, Java, Kotlin를 지원합니다.
따라서 위와 같은 언어 정의 및 언어 판별 규칙을 기반으로 다양한 언어에 대한 하이라이팅을 지원할 수 있습니다. 이때, 해당 Syntax Highlighter에서 기본적으로 지원하지 않는 언어를 서비스 측에서 사용하고 싶다면 어떻게 해야 할까요? 간단하게 언어 주입을 하면 됩니다. 언어 주입은 다음 단계를 통해 진행할 수 있습니다.
- 서비스 측에서 언어 정의 규칙(
LanguageRegexSet
data class)에 맞춰 원하는 언어의 Syntax를 정의합니다. - 에디터 초기화 단계에 Syntax 주입 함수(에디터 SDK 내부에 정의된 public 함수)를 호출합니다.
기능2: 실시간 하이라이팅
실시간 하이라이팅을 지원하기 위해선 퍼포먼스가 중요합니다. 하지만 정확성 또한 퍼포먼스만큼이나 중요한 요소입니다. 퍼포먼스 및 정확성에 영향을 미치는 요인으로는 (1) 하이라이팅 요청 단위와 (2) 하이라이팅 재진행 범위가 있습니다. 각각에 대해 살펴보도록 하겠습니다.
하이라이팅 요청 단위
1) 특정 입력이 발생할때마다(예: 공백 입력) 하이라이팅을 새로 요청
특정 입력에 대해서만 하이라이팅 요청을 하면, 퍼포먼스적으로는 과부하를 줄일 수 있습니다. 하지만 100% 완전한 실시간 하이라이팅은 지원할 수 없습니다.
2) Character 단위로 입력/삭제/수정이 발생할 때마다 하이라이팅을 새로 요청
Character 단위로 하이라이팅을 요청할 경우, 100% 정확도로 실시간 하이라이팅을 지원할 수 있습니다. 하지만 위 영상에서 보이듯이 퍼포먼스 저하로 인해 반응성이 느려집니다.
저는 완전한 실시간 하이라이팅을 지원하기 위해 Character 단위의 하이라이팅을 적용했습니다. 대신 하이라이팅 재진행 범위를 조절해 퍼포먼스를 개선했습니다.
하이라이팅(Tokenizing, Lexing, Color Highlighting) 재진행 범위
1) damaged area only

입력/삭제/수정으로 인해 영향을 받은 부분에 대해서만 하이라이팅을 새로 진행합니다. 변경된 부분에 대해서만 하이라이팅을 진행해 효율적이지만, 변경된 부분을 100% 정확하게 계산하기는 어렵기 때문에 정확성이 떨어질 수 있습니다.
2) whole area

입력/삭제/수정이 발생한 위치와 무관하게 전체 코드에 대해 하이라이팅을 새로 진행합니다. 정확성은 보장할 수 있으나, 퍼포먼스 저하가 발생 가능합니다.
저는 위 두 가지 방법을 혼합해 하이라이팅을 재진행하는 방식을 택했습니다. Tokenizing 및 Lexing엔 오랜 시간이 걸리지 않지만, Color Highlighting에는 안드로이드 플랫폼 특성상 오랜 시간이 걸리는 점을 고려했습니다. 혼합 방식은 다음과 같습니다.
- Tokenizer 및 Lexer: 하이라이팅 요청이 들어오면, 전체 코드에 대해 분석을 새로 진행합니다. (whole area)
- Color Highlighter: 하이라이팅 요청이 들어오면, 새로 Coloring이 필요한 Token에 대해서만 Coloring를 진행합니다. (damaged area only)
따라서 최종 실시간 하이라이팅 과정은 다음과 같습니다.

- Character 변화 단위로 하이라이팅을 새로 요청합니다.
- 하이라이팅 요청이 들어오면, 전체 코드에 대해 Tokenizing과 Lexing을 다시 진행합니다.
- 변경된 Token에 대해서만 Color Highlighting 작업을 진행합니다.
최종 결과물
최종적으로 구현된 Code Syntax Highlighter는 어떤 모습일까요? 다음 영상을 통해 직접 확인하실 수 있습니다.
마무리 글
이로써 Code Syntax Highlighter 개발 과정에 대해 살펴보았습니다. 사실 개인적으로는 아쉬움도 많이 남습니다. 컴파일러 이론에 대한 제대로 된 이해 없이 과제를 진행하기도 했고, 여러 개선 사항들 또한 아직 남은 상태입니다. 하지만 평소 사용만 해보았던 Code Syntax Highlighter를 직접 구현해 보았다는 점에서 재밌는 경험이었다고 생각합니다.
올 하반기에는 부족한 점들을 개선하여 현재 구현한 Code Syntax Highlighter를 라이브러리화하고, 이를 현 SmartEditor에 적용해보는 것을 목표로 하고 있습니다. 나중에 더 발전된 Code Syntax Highlighter로 찾아뵐 수 있길 기대하며, Code Syntax Highlighter 개발기는 여기서 마치겠습니다.
긴 글 읽어주셔서 감사합니다. 😊