[서론 : 프로젝트에 대한 소개]
해당 프로젝트는 "납득 가능한 외래어 순화 인공지능 서비스"라고 설명할 수 있겠다.

AI시대가 되면서 우리나라에는 외래어가 물밀듯이 들어오고 있다. 최근 설문에 따르면 "신문·방송에서 사용하는 말의 의미를 몰라 곤란하다”는 응답이 89%에 달할 만큼 외래어 사용이 증가하고 있다. 국민들의 알 권리가 저하되기 때문에 정부에서는 신문이나 공문서에서 우리말 사용을 지향하는 "공공언어" 사용을 권장한다. 그러나 "공공언어"를 만드는 주체인 국립국어원이 제안한 다듬은 말은 "다듬은 말이 맥락에 맞지 않아 사용하기 힘들다.", "다듬은 말이 납득이 되지 않는다."라는 반응으로 사용되지 않는다.
이러한 문제점을 기술로 해결할 수 없을까하는 고민에서 프로젝트는 시작되었다. 사용자들의 다듬은 말에 대한 선호도를 수집한 뒤 그것을 인공지능이 판단하여 순화어를 제안하는 아이디어이다.
본문에서는 프로젝트의 기술적인 어려움에 대한 해결과정을 다루고 있다.
해당 인공지능에 대한 연구가 궁금하다면 팀의 AI 책임자인 권민재 군이 쓴 논문을 구경해 주시길 바란다.
https://www.dbpia.co.kr/journal/articleDetail?nodeId=NODE12579655
인공지능 기반 외래어 감지 및 순화어 추천 | DBpia
권민재 | 한국정보기술진흥원 학술지 | 2026.3
www.dbpia.co.kr
[본론]
0. 무슨 일을 담당했는가
해당 프로젝트에서 나는 기획, PM, 디자인, 프론트, 데이터 수집 쪽을 담당하였고, 본 글에서는 <순화 API 호출을 줄이는 과정>에서의 트러블슈팅을 다룬다. 먼저 프론트 화면에서 수행해야 할 과정은 다음과 같다.
<문서편집과 문서의 외래어를 순화해주는 화면>
1. 문서편집기의 외래어는 하이라이트된다.
2. 오른쪽 사이드바에는 감지된 외래어와 그 외래어에 대한 다듬은 말이 추천된다.
3. 다듬은 말에 대한 제안을 승인하면 문서편집기에서 그 단어가 다듬을 말로 변경된다
1. API 호출이 멈추질 않는다.
먼저 초안은 문서에디터가 수정되면 순화 요청을 보내는 것이다.
useEffect(()=>{
순화API호출(documentContent)
},[documentContent])
당연히 이렇게 하면 한 글자 수정할 때마다 순화 요청이 날아가는 참극이 발생할 것이기에 "디바운스" 도입을 선택했다.
디바운스(Debounce)는 프로그래밍에서 연속적으로 발생하는 이벤트를 그룹화해 불필요한 호출을 방지하는 기술이다.
2. useEffect를 활용하여 디바운스를 구현해 보자.
useEffect는 컴포넌트 렌더링 후 부수적인 효과를 실행하는 Hook인데, 내부적으로는 React가 Fiber 노드의 updateQueue에 Effect를 저장해 두고, DOM 커밋 단계 이후 다음 이벤트 루프에서 실행된다. 이 함수는 하나의 useEffect 버전이 존재해야 하므로 새로운 버전의 변경이 호출되면, 이전 버전의 useEffect는 제거된다. 이때 제거된 useEffect에는 제거될 때 액션을 취할 수 있는데, 여러분들이 잘 아는 Clean-up이다. 단순히 컴포넌트가 Unmount 될 때 트리거된다고 생각할 수 있지만, 모든 것이 React Fiber로 이루어진 React세계의 관점에서는 Component가 unmount 되면서 (Component를 React에 등록한 Fiber의 UpdateQueue로 등록되어 있던) useEffect 인스턴스가 자체가 unmount 되기 때문에 Clean-up이 동작하는 것이지 Component의 unmount만 관심사로 둔 것이 아니다.
이걸 활용하면 디바운스를 쉽게 구현할 수 있다.
useEffect(() => {
const timeout = setTimeout(() => {
순화API호출(documentContent);
}, 3000);
return () => clearTimeout(timeout);
}, [documentContent]);
문서가 변경되면 useEffect 안에서는 setTimeout으로 3000ms 뒤에 콜백함수가 실행된다. 여기서 새로 변경이 되면 기존 useEffect 인스턴스는 Clean-up을 통해 setTimeout이 해제됨으로 실행되지 않는다. 결론적으로 언제나 3000ms 후 아무 변경도 하지 않아야 API가 호출되는 디바운스를 만들 수 있다.
여기서 3000ms로 잡은 이유는 Google Docs나 Notion 같은 문서편집기가 대게 2~3초의 디바운스를 적용하는 데에서 착안했다.써봤을 때도 가장 좋다.
3. 아직도 불필요한 API 호출은 존재한다.
디바운스를 도입했음에도 아직 불필요한 호출은 존재한다. 말하자면 이런 상황이다.
"수많은 문장들 중에 한 문장만 수정했는데, 변경하지 않은 문장들까지 순화 요청이 되고 있어요."
뭐? 변경이 안 됐는데 요청에 포함되고 있다고? 변경을 추적해야겠군. 뭐? 변경을 추적한다고? 이거 완전 Git이잖아.

4. 변경점만 찾아내는 코드를 만들어보자.
변경점을 찾아내기 위해서는 먼저 변경의 단위를 정의하는 것이 필요할 것이다. Git이 파일 단위로 변경사항을 추적하듯, 이 시스템에서도 문장을 추적의 기본 단위로 삼았다. 고맙게도 문서편집기 구현에 사용한 CKEditor 같은 경우는 내용을 블록 단위로 분리하는 구조를 가지고 있다. 해당 블록을 변경의 단위로 삼았다.
예를 들면 h1으로 쓴 블록과 p로 쓴 블록이 분리되어 있기 때문에 해당 블록을 한 문장의 단위로 가정하겠다는 것이다.
변경의 단위를 정했겠다. 이제 버전을 관리해야 할 것이다. 버전 관리는 간단하게 2-State구조를 채택했다. 전 버전과 현재 버전만 관리하겠다는 것이다. 버전 비교를 위해서 좋은 방법도 많겠지만 "deep-diff" 라이브러리를 활용했다. diff(lhs, rhs) 한 번으로 문장이 변경됐는지, 생성됐는지, 제거됐는지, 유지됐는지 알 수 있다.
[
{
"kind": "E",
"path": ["title"],
"lhs": "오늘은 날씨가 좋아",
"rhs": "오늘은 날씨가 흐려"
},
{
"kind": "N" // 새로 추가
"path": ["newField"],
"rhs": "추가된 필드"
}
]
5. 이전 버전의 응답이 지연돼서, 현재 버전과 충돌이 난다.
아직 문서편집기의 최적화는 끝나지 않았다. 상황은 다음과 같다.
v1에서 요청을 보낸다. 그동안 문서가 수집되어, v2 버전에서 요청이 호출되었다.
그런데 v1의 응답값이 이제야 돌아오고 있다! v2의 수정사항과 충돌이 났다.
비교적 간단한 문제이다. 응답값이 버전에 대한 정보를 같이 반환해 주면 된다.
이 "순화 응답은 v1를 지칭하고 있습니다."
음 현재는 v3이기 때문에 v1는 적용하지 않고 폐기해야겠군.
6. 유저가 대용량 텍스트를 복붙 하면 어떻게 될 것인가?
디바운스를 통해 장기적인 편집의 요청을 최소화했다. 하지만 단기적인 상황에서는 어떻게 할 것인가? 유저가 1000자의 텍스트를 복사/붙여 넣기를 한다면? 뭐라도 최적화하고 싶다.
우리 서비스의 순화 프로세스는 다음과 같다. 서버로부터 응답이 돌아올 때까지 유저 화면상에는 스피너가 돌고 있을 뿐이다.
[순화 요청이 필요함] --[서버로 전송]-> [외래어 감지] -> [외래어가 감지된 문장만 순화 AI 적용] -> [반환]
좋은 아이디어가 떠올랐다. 외래어 감지를 클라이언트 측에서 실행시키자.
우리 팀의 외래어 감지 모델은 LSTM 경량 모델이다. (이것도 어떻게 했는지 궁금하다면 여길 구경하길 바란다.)
외래어 감지 모델은 클라이언트 측으로 이전시키면
- 클라이언트 입장: 외래어가 감지되며 화면상에 붉은 하이라이트 먼저 표시되며 몇 초 후에 순화어가 반환됨.
- 서버 입장: 외래어가 감지된 문장들만 서버에 오기 때문에 훨씬 좋음.
따라서 서버의 LSTM(TensorFlow.py)을 클라이언트 단에서 TenserFlow.js로 실행하였다.
(모델을 Tensor Flow SavedModel로 변환 후 tfjs_converter 사용한 뒤 브라우저에서 호출했습니다.)
여담) 외래어 감지 모델은 정말 경량 모델이라 실행되지 않는 컴퓨터는 없을 것이긴 하나,
클라이언트 환경을 체크해서 외래어 감지 모델의 실행을 백엔드 측에서 실행해 주는 Fail-Safe 대안을 마련해 두는 것도 좋을 것 같다.
7. 개선된 성능을 측정해 보자.
이론상 개선된다는 것을 알았지만, 개선된 버전이 얼마나 효율적으로 API를 줄였는지 측정하고 싶다. 현업의 개발자라면 A/B테스트를 통해서 실제 시나리오를 검증하고 정량화할 수 있으나, 사용자가 없는 가난한 개발자라면 실제상황을 재현해야 할 것이다.
목표는 A상황과 B상황 모두 동일한 상황에서 API 호출수를 측정하고 싶다. 인간이 아무리 노력한들 똑같은 문서 편집을 두 번 재현하는 능력은 무리이다.
"클라이언트의 클릭, 입력 이벤트를 모조리 기록할 수 없을까?"
감사하게도 이미 이것을 한 개발자가 있다. 클라이언트의 텍스트 입력과 같은 사용자 행동을 JSON으로 저장해 주고 실행까지 해주는
rety 라이브러리가 있다. 이것을 이용해 문서편집 시나리오 10개 정도를 저장하고 개선 전 버전(디바운스까지 적용)과 개선 후 버전(모든 개선 적용)에 실행시켰다. 대용량 텍스트 복사/붙여 넣기 같은 상황을 제외한 나머지 시나리오에서 모두 60% 이상의 감소를 이끌어냈다.