🧐 문제의 시작: “왜 검색 결과가 다르지?”
“힛탠다드”와 같은 검색어를 직접 입력했을 때는 문제가 없었어요. 하지만 "힛텐다드"와 같이 잘못된 검색어를 입력했을 때,
검색 서비스에서 제공하는 검색어 제안 기능으로 "힛탠다드"가 검색이 되면 NR(검색 결과 없음)으로 표시되는 이슈가 발생했습니다.
분명 같은 키워드인데 왜 어떤 상황에서는 검색이 잘되고, 다른 상황에서는 검색 결과가 나오지 않을까요?
🔍 문제 추적 과정: 어디서부터 차이가 생겼을까?
첫 번째 단서: encodeURIComponent로 인한 문자열 길이 차이 발견
검색어가 제안되었을 때만 NR(검색 결과 없음)이 발생하는 것을 발견하고, 검색어 제안 과정에 문제가 있을 가능성을 의심했습니다.
이때 프론트엔드 개발자분이 encodeURIComponent("힛탠다드")로 인코딩한 결과, 문자열의 길이 차이를 발견해 주셨습니다.
이를 통해 키워드가 다르게 인식되고 있음을 확인하게 되었습니다.
두 번째 단서: 유니코드 분석으로 확인한 NFD와 NFC 차이
문자열 길이 차이의 원인을 더 자세히 파악하기 위해, 유니코드 코드 포인트로 두 검색어를 분해하여 비교했습니다.
그 결과, 자동 제안된 키워드가 NFD 방식으로 분해된 상태라는 사실을 발견했습니다.
예시:
NFC(정상적인 검색어): 힛탠다드
코드 포인트: U+D79B U+D0E0 U+B2E4 U+B4DC
NFD(자동 제안된 검색어): 힛탠다드
코드 포인트: U+1112 U+1175 U+11BA U+1110 U+1162 U+11AB U+1103 U+1161 U+1103 U+1173
제안된 키워드가 NFD 방식으로 분리된 형태로 제공되어, 검색 엔진에 NFC 방식으로 저장된 데이터와 일치하지 않아 검색 결과가 나오지 않았던 것이 원인이었습니다.
🔤 문자셋과 인코딩에 대하여
문제를 이해하기 위해 문자셋과 인코딩의 개념을 간단히 살펴보겠습니다.
문자셋(Character Set)
문자셋은 컴퓨터가 문자들을 다루기 위해 각 문자에 부여한 고유한 코드의 집합입니다.
대표적인 문자셋:
- ASCII 코드: 영어 알파벳, 숫자, 특수 문자 등을 포함하는 7비트 문자셋으로 구성되며 총 128개의 문자를 표현
- 예: A는 ASCII 코드로 65에 해당.
- 유니코드(Unicode): 전 세계의 모든 문자를 통합적으로 표현하기 위한 문자셋
인코딩(Encoding)
인코딩은 문자셋에 정의된 문자 코드를 컴퓨터 시스템에서 실제로 저장하고 전달하는 방식입니다.
대표적인 인코딩 방식:
- UTF-8: 가변 길이 인코딩으로, ASCII 문자는 1바이트, 한글 등은 2~4바이트로 표현
- UTF-16: 대부분의 문자를 2바이트로 표현
🛠 문제 원인: NFD와 NFC 정규화 방식의 차이
유니코드와 한글
유니코드는 완성형 문자와 조합형 문자 모두를 지원합니다.
완성형 문자: 자음과 모음이 결합된 하나의 음절을 하나의 코드 포인트로 표현합니다.
조합형 문자: 자음과 모음을 개별적인 코드 포인트로 분리하여 표현합니다.
이처럼 유니코드에서는 동일한 문자를 여러 가지 방법으로 표현할 수 있기 때문에, 이를 표준화하기 위한 정규화(Normalization) 방식이 필요합니다.
이 정규화 방식에는 NFC, NFD 등이 있습니다.
NFC (Normalization Form Composed)
문자를 완성형 문자로 조합하여 표현하는 방식입니다.
예시: '힛탠다드'를 완성형 코드 포인트로 표현하면 각 음절이 하나의 코드 포인트로 나타납니다.
'힛': U+D79B
'탠': U+D0E0
'다': U+B2E4
'드': U+B4DC
NFD (Normalization Form Decomposed)
문자를 분해된 자모 문자로 표현하는 방식입니다.
예시: '힛탠다드'를 자모로 분해하여 표현하면 각 음절이 자음과 모음의 조합으로 나타납니다.
'힛': ‘ᄒ’ (U+1112) + ‘ᅵ’ (U+1175) + ‘ᆺ’ (U+11BA)
'탠': ‘ᄐ’ (U+1110) + ‘ᅢ’ (U+1162) + ‘ᆫ’ (U+11AB)
'다': ‘ᄃ’ (U+1103) + ‘ᅡ’ (U+1161)
'드': ‘ᄃ’ (U+1103) + ‘ᅳ’ (U+1173)
왜 문제가 발생했을까?
이번 문제는 최근 검색어 제안 시스템을 이관하는 과정에서 제안된 응답을 NFC 방식으로 정규화하는 과정이 누락되면서 발생했습니다.
제안된 키워드가 NFD 방식으로 응답되었지만, 브라우저 상에서는 완성형처럼 보여 문제가 있는지 사전에 파악하기가 어려웠던 것이죠.
그 결과, 검색어 제안 기능에서 NFD로 정규화된 키워드가 그대로 검색 요청에 사용되었고, NFC 방식으로 저장된 검색 엔진 데이터와 일치하지 않아 검색 결과가 나오지 않는 상황이 발생했습니다.
결국, 정규화 방식의 불일치로 인해 같은 키워드임에도 불구하고 검색 엔진에서 매칭되지 않는 문제가 생긴 것입니다.
🔎 검색어 제안 시스템에서는 왜 NFD를 사용했을까?
한글의 특성과 편집 거리
한글은 자모(자음과 모음)로 이루어져 있어, 철자 오류나 오타에 민감합니다. 한 글자만 바뀌어도 단어의 의미가 완전히 달라질 수 있죠.
예시: "사랑", "사물", "사장", "사돈"
위 네 단어는 한 글자 차이지만, 의미는 완전히 다릅니다.
자소 분리를 하지 않고 단어 전체를 비교하면 편집 거리가 동일하게 계산되어 의도하지 않은 제안이 이루어질 수 있습니다.
자소 분리를 통한 정확한 편집 거리 계산
자소 분리를 통해 단어를 초성, 중성, 종성 단위로 나누면, 더 세밀한 편집 거리 계산이 가능합니다.
예시: 힛탠다드와 힛텐다드
자소 분리 후 편집거리를 비교하면 ㅐ와 ㅔ의 차이만 인식하여 사용자가 의도한 검색어를 제안할 수 있습니다.
🛠 문제 해결: 유니코드 정규화 일관성 유지하기
검색어 제안 API에서 응답을 제공할 때, 유니코드 정규화(NFC 방식)를 적용하여 문제를 해결했습니다.
이를 통해 NFC 방식으로 저장된 검색 엔진 데이터와 정규화 규약이 일치하게 되었고, 제안된 키워드로도 정상적인 검색이 가능해져 문제가 해결되었습니다.
⌨️ 번외. 플랫폼별 NFD와 NFC 렌더링 방식 차이
크롬 브라우저에서의 NFD와 NFC 처리
크롬 브라우저에서는 NFD와 NFC 정규화 여부와 상관없이 한글을 일관된 완성형 문자인 것처럼 렌더링합니다. 이로 인해 NFD와 NFC의 차이가 시각적으로 드러나지 않아 처음 문제를 파악하는 데 어려움이 있었습니다.
NFD면 당연히 쪼개져서 표현될 거라 생각했거든요.🥲
macOS에서의 NFD와 NFC 처리
macOS는 기본적으로 NFD 방식을 사용하며, NFC와 NFD를 모두 지원합니다. 이 때문에 힛탠다드(NFC)라는 이름의 폴더를 생성한 후, 힛탠다드(NFD) 라는 이름의 폴더를 생성하려고 하면 같은 이름으로 인식되어 충돌이 발생합니다.
이는 macOS 파일 시스템이 두 정규화 방식을 동일하게 취급하기 때문입니다.
Windows에서의 NFD와 NFC 처리
Windows에서는 기본적으로 NFC 방식만을 사용하여 한글을 처리합니다.
이러한 이유로 NFD로 인코딩된 한글은 Windows 환경에서 자모가 분리된 형태로 변환되는 것을 확인했습니다.
예를 들어, NFD 방식의 힛탠다드를 폴더 이름으로 지정하려고 하면 "ㅎ ㅣ ㅅ ㅌ ㅐ ㄴ ㄷ ㅏ ㄷ ㅡ"처럼 자음과 모음이 분리된 형태로 표시됩니다.
안드로이드에서의 NFD와 NFC 처리
흥미롭게도 안드로이드 14에서는 NFC와 NFD 간에 폰트 렌더링 차이가 있어 구분이 가능했습니다.
NFD로 인코딩된 한글은 글자 모양이 미세하게 다르게 표시되는 것이 확인되었습니다.
📝 마무리 및 느낀 점
이번 문제는 겉보기에는 작은 해프닝처럼 보일 수 있지만, 이를 해결하는 과정에서 데이터 처리 일관성의 중요성과 한글 검색어 처리의 중요성을 다시 한 번 깨닫게 되었습니다.
배운 점 1: 데이터 처리 일관성의 중요성
시스템 전반에서 데이터 처리 방식을 일관성 있게 유지하는 것이 얼마나 중요한지 알게 되었습니다. 과거에 빅 엔디안과 리틀 엔디안 간의 차이로 발생한 NUXI 문제처럼, 데이터 처리 방식의 불일치는 서비스 안정성과 사용자 경험에 직접적인 영향을 줄 수 있다는 사실을 다시금 실감했습니다.
배운 점 2: 협업의 가치
이번 문제를 해결하는 데 있어 프론트엔드 개발자와의 협업이 큰 도움이 되었습니다. 서로 협력함으로써 문제를 더 빠르고 정확하게 해결할 수 있었습니다. 팀원 간의 원활한 소통이 얼마나 중요한지 다시 한 번 느꼈습니다.