텍스트 길이 제한을 다른 언어로 옮길 때는 숫자보다 무엇을 세는지부터 확인해야 한다. 같은 문자열도 바이트, UTF-16 코드 단위, 유니코드 코드 포인트의 개수가 다르다. 화면에 보이는 글자 수와도 같은 기준이라고 가정할 수 없다.

기능을 유지한 별도 Rust판을 만드는 작업에서 문자열 길이 문제가 드러났다. 기록과 코드를 읽어 보니 원래 프로그램도 한 가지 단위만 쓰지 않았다. 항목별 검증 라이브러리와 직접 작성한 전체 합계식이 서로 다른 길이를 세고 있었다.

같은 입력을 서로 다른 단위로 세었다

이번 글의 독립 합성 프로그램에서 Rust의 바이트 길이, chars로 센 스칼라 값의 개수, encode_utf16으로 센 코드 단위 수를 기록했다. Node에서도 UTF-8 바이트 수, 문자열을 펼쳐 센 값, length를 각각 기록해 같은 입력끼리 대조했다. 원래 브라우저나 그 제품 시험을 실행한 것은 아니다.

단위 차이는 한글과 이모지만 넣어도 드러났다. 아래 값은 이번에 실행한 합성 입력의 측정 결과다. 눈으로 보이는 폭이나 글자 묶음의 개수를 측정한 표가 아니다. 이번 정상 유니코드 입력에서는 Rust 스칼라 수와 Node에서 센 코드 포인트 수가 같았다.

합성 문자열 측정 · 순서는 UTF-8 바이트 / 코드 포인트에 대응한 스칼라 수 / UTF-16 단위다.

코드와 구현 살펴보기

합성 문자열 측정 · 실제 화면 글자 수 검사 아님

a       : 1 / 1 / 1
가      : 3 / 1 / 1
😀      : 4 / 1 / 2
e + U+0301 : 3 / 2 / 2
👩 + U+200D + 💻 : 11 / 3 / 5

언어의 기본 길이보다 실제 검증기를 먼저 읽었다

확인한 원래 프로그램에는 Zod 4.6.7이 설치돼 있었다. 해당 버전의 문자열 min/max 구현은 코드 포인트 수를 확인하도록 되어 있었다. JavaScript 문자열의 length가 UTF-16 단위라는 이유만으로 이 라이브러리의 한도까지 같은 단위라고 판단하면 원래 허용하던 입력을 거절할 수 있다.

이를 작게 확인하려고 제품의 실제 한도가 아닌 합성 한도 2를 사용했다. 설치된 라이브러리의 max 검사에 이모지를 넣었을 때 1개와 2개는 허용됐고 3개는 거절됐다. 이모지 2개의 JavaScript length는 4였지만 검증기는 허용했다. 라이브러리가 무엇을 비교하는지 실제 결과로 구별한 것이다.

집필 당시 확인한 Rust 소스는 이 문자열 한도를 chars().count()로 비교한다. 예전 길이 판단이 원본에서 허용하던 이모지 입력을 거절해 수정했다는 작업 기록도 있다. 당시 제품 시험을 반복하지는 않았다. 설치 버전과 소스 범위를 보존하고 독립 합성 입력으로 허용·거절의 경계를 대조했다.

항목별 제한과 전체 합계는 같은 계산이 아니었다

원래 노트북 코드에서는 각 셀의 source를 검증 라이브러리로 제한하고, 전체 source 합계는 문자열 length를 더해 별도로 검사한다. 따라서 항목별 한도는 코드 포인트, 전체 합계는 UTF-16 단위가 된다. 같은 소스 문자열을 검사하더라도 두 조건의 단위가 같지 않은 것이다.

검토한 Rust 코드도 두 계산을 구분한다. 개별 문자열 한도에는 스칼라 수를 쓰고, 전체 합계에는 encode_utf16().count()를 더한다. 공통 길이 함수 하나로 통일하면 기존에 허용하던 입력이 달라질 수 있다. 단위를 통일하려면 언어 전환과 별개의 동작 변경으로 검토해야 한다.

독립 합성 예시에서는 항목별 한도 2와 전체 UTF-16 한도 4를 뒀다. 이모지가 하나씩 든 셀 2개는 합계 4로 통과했고, 셀 3개는 개별 검사를 모두 통과해도 합계 6으로 거절됐다. Rust와 Node의 합성 확인에서 같은 결과가 나왔다. 이 작은 한도는 실제 제품 설정이 아니다.

검사식 옆에 단위와 경계값을 적는다

기능을 옮기기 전에 입력마다 검사식, 단위, 경계값, 정규화 순서를 비교표에 적는다. “최대 글자 수”라고만 써 두면 어디에서 바이트로 바뀌었는지, 라이브러리가 다른 기준을 쓰는지 찾기 어렵다.

같은 기준표라도 실제 검증 라이브러리와 사용 버전을 함께 적어야 한다. 다른 버전이나 다른 함수가 같은 규칙을 쓴다고 확대하지 않는다. 정렬이나 주소 해석처럼 다른 호환성 문제까지 문자열 길이 확인 하나로 검증했다고 말하지 않는 것도 같은 이유다.

  1. 저장·전송 용량: 어떤 인코딩의 바이트를 세는지 적는다.
  2. 검증 라이브러리의 문자열 한도: 실제 버전과 허용·거절 결과를 남긴다.
  3. 직접 작성한 합계식: 개별 항목 검사와 별개의 단위인지 확인한다.
  4. 화면의 글자 묶음과 줄바꿈: 길이 한도와 분리해 글꼴·폭·분절 규칙을 확인한다.

보이는 한 글자와 코드 포인트도 구별한다

합성 입력에는 e 뒤에 결합 문자를 붙인 문자열과 연결 문자가 든 이모지 문자열도 넣었다. 계산된 스칼라 수와 UTF-16 단위 수를 기록했지만, 이 결과로 사용자가 화면에서 몇 글자로 읽는지 판단하지 않았다. 코드 포인트를 센다는 말은 화면의 폭이나 글자 묶음을 확인했다는 말이 아니다.

사용자가 보는 글자 묶음을 제한하려는 제품이라면 그 요구를 별도로 정해야 한다. 분절 기준과 편집 동작, 줄바꿈을 실제 화면에서 확인할 필요가 있다. 기존 한도를 그대로 옮기는 일과 더 나은 입력 정책을 새로 정하는 일을 한 번에 섞으면 어느 변경이 호환성을 바꿨는지 알기 어려워진다.

이번 합성 입력은 정상 유니코드 문자열에 한정한다. 단독 서로게이트, 정규화 방식, 실제 브라우저 렌더링이나 입력기의 조합 동작까지 시험한 것은 아니다. 계산값이 일치한 범위를 적고, 전체 문자 처리가 검증됐다는 표현은 피한다.

읽은 코드와 새 시험 결과의 범위를 맞췄다

이번에는 합성 문자열 5개의 계산값과 한도 경계 사례 5개를 비교했다. Node에서 설치된 검증 라이브러리를 재사용하고, Rust에서는 같은 예시의 스칼라·UTF-16 계산과 경계식을 독립적으로 실행했다. 이 결과는 선택한 예시의 일치이며 원래 프로그램 전체의 기능 동등성을 증명하지 않는다.

검토한 시점에는 원래 Rust 전환 작업과 실제 화면 검수가 별도 담당의 진행 과제로 남아 있었다. 이 글을 쓰며 그 소스를 수정하거나 업무 창·로그인·탭을 열지는 않았다. 위의 합성 결과와 과거 제품 시험 기록은 각각의 시점과 범위로 읽어야 한다.