사용자 입력 텍스트를 바이트(byte) 길이로 제한하는 Vue 컴포넌트 만들기


스펙 소개

  NHN Cloud 서비스를 이용하기 위한 관리 도구인 Console 웹 애플리케이션의 프런트엔드 개발 중 사용자가 입력한 글자를 100 바이트 길이로 제한하는 컴포넌트 구현이 필요했다.

NHN Cloud Console은 NHN Cloud 서비스를 이용하기 위한 관리 도구이다. 서비스를 이용하기 위한 조직과 프로젝트와 같은 기본 정보를 관리하고, 서비스를 활성화/비활성화할 수 있으며, 해당 서비스를 이용하는 멤버를 관리할 수 있다. 또한 서비스 이용 내역에 대한 결제 정보를 제공한다. NHN Cloud Console의 좀 더 자세한 기능을 알고 싶으면 가이드를 참고한다.

  여기서 잠깐, Console 웹 애플리케이션의 프런트엔드 개발 환경을 소개하자면 Vue2로 개발되어있으며, 향후 공통으로 사용할 UI Kit을 제공할 목적으로 모노레포를 구성하였다. yarn workspace를 사용하며 코딩 컨벤션을 위한 eslint, prettier가 적용되어 있다. 또한 빌드와 개발 서버를 띄우기 위해 Webpack4를 사용하고 있으며, 통합 테스트를 위한 testing-library 를 적용하여 TC를 작성 중이다.


다시 해야 할 일로 돌아와 구현해야 할 상세 스펙은 아래와 같다.
- 사용자는 한글/영문/숫자/특수문자 자유롭게 입력할 수 있다.
- 입력하는 글자는 100바이트 길이로 제한한다.
- 100바이트 미만의 글자만 표시한다. 초과하는 바이트의 입력은 무시한다.

  이 글에서는 입력한 글자의 바이트 길이를 계산하는 함수를 작성하고, 위에 스펙을 지원하는 재사용 가능한 컴포넌트를 만들 것이다.

구현하기에 앞서...

  구현하기에 앞서 글자를 바이트 길이로 계산하기 위한 배경지식으로 컴퓨터가 처리할 수 있는 최소 단위인 비트와 바이트의 개념에 대해 알아보고 텍스트 인코딩에 대해 간단히 설명한다.

바이트란?

  바이트를 설명하기 위해서는 먼저 비트(bit)를 알아야 한다. 비트는 Binary Digit의 줄임말로 컴퓨터가 이해할 수 있는 정보의 최소 단위이다. 0 또는 1 정보를 담을 수 있으며 비트를 조합하여 더 큰 범위의 데이터를 다룰 수 있다. 2비트이면 2^2(2의 2제곱)로 4가지의 경우를 나타낼 수 있으며 8비트는 2^8(2의 8제곱)로 256가지의 다른 정보를 담을 수 있다. 8비트는 1바이트가 되는데, 컴퓨터의 기억장치(Computer data storage)의 크기를 나타내는 단위로 자주 쓰이며, 프로그래밍에서는 컴퓨터가 데이터를 처리할 때 가장 기본적인 단위가 된다. 프로그래밍에서 변수 선언 시 데이터 타입에 따라서 메모리에 공간 확보에 영향을 미칠 수 있기 때문에 데이터 타입을 알맞게 설정하여 메모리를 효율적으로 사용하는 것이 좋다. 또한 컴퓨터에서 텍스트의 단일 문자를 인코딩하는데 사용된 비트 수를 의미하기도 한다.

  바이트를 기준으로 더 큰 사이즈의 단위를 정의하기 위해 다양한 단위가 존재한다. 컴퓨터 네트워크의 데이터의 속도 단위, 하드 디스크 드라이브와 플래시 매체의 전송 속도, 대부분의 저장 매체(하드 디스크 드라이브, USB 플래시 드라이브, DVD 등)에서 10진법을 사용하여 10의 거듭제곱으로 계산된 이 단위(킬로-kilo, 메가-mega, 기가-giga, 테라-tera ...)를 사용한다.

값(byte) 단위
1000 KB - kilobyte
1000^2 MB - megabyte
1000^3 GB - gigabyte
1000^4 TB - terabyte

  또 다른 시스템(메인 메모리와 CPU 캐시 크기, RAM 용량 등)에서는 2진법을 사용하여 2의 거듭제곱으로 계산되어 정의한 단위를 사용한다. binary 프리픽스를 붙여(키비-kibi, 메비-mebi, 기비-gibi, 테비-tebi...) 부르기로 하였다.

값(byte) 단위 레거시
2^10 KiB - kibibyte KB - kilobyte
2^20 MiB - mebibyte MB - megabyte
2^30 GiB - gibibyte GB - gigabyte
2^40 TiB - tebibyte TB - terabyte

  10진법과 2진법을 사용해서 계산한 값은 킬로바이트에서는 차이가 크게 나지 않지만, 단위가 커질수록 실제 값의 차이가 크게 나며, 혼란을 줄 수 있으므로 10의 거듭제곱으로 계산한 단위((kilo, mega, giga, tera ...)와 2의 거듭제곱으로 계산한 단위(kibi, mebi, gibi, tebi...)의 차이를 정확히 알고 사용하자.

그렇다면 문자를 인코딩하려면?

  문자 인코딩(Character encoding)이란 그래픽 문자, 사람이 사용하는 언어로 쓰인 문자를 디지털 컴퓨터를 사용하여 저장, 전송 및 변환할 수 있도록 숫자에 할당하는 프로세스이다. 쉽게 말해, 사용자가 입력한 문자나 기호를 컴퓨터가 이해할 수 있는 숫자로 이루어진 신호로 만드는 것이다. 가장 처음 만들어진 텍스트 인코딩은 아스키(ASCII)이다. 아스키 코드표를 확인해보면 출력 불가능한 제어 문자 33개와 영문 대소문자, 숫자, 특수문자, 공백문자와 같은 출력 가능한 문자 95개로 총 128(2^7 → 7비트)개로 이루어져 있다. 1바이트를 구성하는 8비트 중 7비트만 사용하는 이유는 나머지 1비트가 통신 에러 검출을 위한 용도로 쓰이기 때문이다. 이러한 나머지 1비트는 패리티 비트(Parity Bit)라고 부른다. 하지만 아스키에 정의된 코드만으로는 여러 국가의 언어를 인코딩하는데 한계가 있으므로, 나라별 언어를 모두 표현하기 위해 나온 코드 체계가 유니코드(Unicode - Universal Coded Character Set)이다. 유니코드는 전 세계의 모든 문자를 컴퓨터에서 일관되게 표현하고 다룰 수 있도록 설계된 산업 표준이다. UTF-8(Unicode Transformation Format – 8-bit)은 전자 통신에 사용되는 가변 길이 문자 인코딩이다. 글자마다 바이트 크기가 다르게 계산되는 방식을 가변 방식이라 하는데 유니코드 한 문자를 나타내기 위해 1바이트에서 4바이트까지 사용한다. 한글은 유니코드에서 U+AC00에서 U+D7A3의 범위를 가지는데 총 11,172개의 한글을 표현할 수 있다. U+AC00에서 U+D7A3의 범위의 글자들은 유니코드 설계상 3바이트로 인코딩한다.

  예시로 '한'이란 글자를 UTF-8로 인코딩되는 과정을 살펴보겠다. '한'의 유니코드의 문자 코드는 U+D55C이며, U+0800에서 U+FFFF까지의 범위 안에 있다. [1110xxxx 10xxxxxx 10xxxxxx]와 같이 3바이트로 인코딩된다.

D는 16진수에서 13을 의미, C는 16진수에서 12를 의미

/* 10진수로 변환 */
const decimalHan = 16**3 * 13 + 16**2 * 5 + 16**1 * 5 + 16**0 * 12 // 54620
//또는
const decimalHan = '한'.charCodeAt(0) // 54620

/* 2진수로 변환 */
const binaryHan = new Number(decimalHan).toString(2) // '1101010101011100'

'1101 0101 0101 1100'를 [1110xxxx 10xxxxxx 10xxxxxx] 차례로 넣으면 11101101 10010101 10011100(3바이트)가 된다.

문자의 바이트 길이 계산?

  UTF-8에서는 기호를 포함한 문자의 바이트 길이는 1바이트에서 4바이트로 계산된다. UTF-16은 아스키 표에 정의된 코드도 1바이트가 아니라 2바이트로 계산한다. 예를 들어, 자바에서 문자열의 바이트 길이를 계산할 때 str.getBytes(characterSet).length로 계산하는데 인코딩 문자셋에 따라 한글 하나를 UTF-8에서는 3바이트로 euc-kr에서는 2바이트로 계산된다. 서버 측 인코딩을 확인하여 아스키코드 외 문자에 대한 바이트 길이를 프런트엔드와 동일하게 설정한다.

구현하기

  이젠 문자의 바이트 길이를 구하는 함수를 작성하고 이것을 사용해 입력 문자열을 바이트 길이로 제한할 수 있는 컴포넌트를 만들어보자.

글자의 바이트 길이 구하기

  입력한 글자가 최대 바이트 제한을 넘지 않게 하려면, 우선 입력된 글자의 바이트 길이를 계산할 수 있어야 한다. 다음은 글자가 몇 바이트인지 계산하는 함수이다.

const LINE_FEED = 10; // '\n'
  
function getByteLength(decimal) {
  return (decimal >> 7) || (LINE_FEED === decimal) ? 2 : 1
}

function getByte(str) {
  return str
    .split('')
    .map((s) => s.charCodeAt(0))
    .reduce((prev, unicodeDecimalValue) => prev + getByteLength(unicodeDecimalValue), 0)
}

  바이트 길이를 계산해 주기 위해 자바스크립트 API charCodeAt메서드와 오른쪽 비트 시프트 연산자(>>)가 사용되었다.

  • String.prototype.charCodeAt(index)는 인덱스에 해당하는 UTF-16의 유니코드 값을 10진수로 반환한다.
  • 반대로, String.fromCharCode(decimal) 을 사용하여 10진수의 값을 인자로 넘겨주면 UTF-16의 유니코드 표의 심볼을 가져올 수 있다. 예) String.fromCharCode(54620) // '한'

  c >> 7unicodeDecimalValue가 0~127 범위에 있는 수인지 확인하는 코드이다. c값이 0부터 127이면 ASCII 영역에 해당되며 1바이트를 사용한다. 128부터는 2바이트로 계산하였다. 추가로 커서를 아래로 이동하여 새로운 행이 추가되는 \n(LF - Line Feed)는 2바이트로 계산하였다.

이해를 돕기 위해 오른쪽 비트 시프트의 연산은 다음과 같다. a >> b = a / (2**b)


글자를 최대 바이트 길이만큼 자르기

  입력된 글자가 최대 바이트 길이를 넘어가게 되면 글자를 최대 바이트 길이까지 잘라야 한다. 다음은 글자를 maxByte 길이만큼 자른 텍스트를 반환하는 함수이다.

function getLimitedByteText(inputText, maxByte) {
  const characters = inputText.split('')
  let validText = ''
  let totalByte = 0

  for (let i = 0; i < characters.length; i += 1) {
  const character = characters[i]
  const decimal = character.charCodeAt(0)
  const byte = getByteLength(decimal) // 글자 한 개가 몇 바이트 길이인지 구해주기
  
      // 현재까지의 바이트 길이와 더해 최대 바이트 길이를 넘지 않으면 
      if (totalByte + byte <= maxByte) { 
        totalByte += byte      // 바이트 길이 값을 더해 현재까지의 총 바이트 길이 값을 구함
        validText += character // 글자를 더해 현재까지의 총 문자열 값을 구함
      } else {                 // 최대 바이트 길이를 넘으면
        break                  // for 루프 종료
      }
  }
  
  return validText
}

  getLimitedByteText함수는 입력한 문자열(inputText)과 제한해야 할 최대 바이트 길이 값(maxByte)을 인자로 받는다. 한 개의 글자가 몇 바이트 길이인지 구해주고 현재까지의 바이트 길이와 더해 최대 바이트 길이를 넘지 않으면 totalBytevalidText에 각 바이트 길이와 현재 글자를 더해준다. 만약 최대 바이트 길이를 넘으면 for 루프문을 종료하며 최대 바이트 길이 값까지 계산된 유효한 문자열을 반환한다.

LimitByteInput 컴포넌트 작성

  자, 텍스트의 바이트 계산을 할 수 있으니 이제 재사용 가능한 LimitByteInput 컴포넌트를 만들어보자. LimitByteInput 컴포넌트의 구현 스펙은 아래와 같다.

- 부모 컴포넌트로부터 props로 넘겨받는 정보
  - maxByte : 제한할 최대 바이트 길이
  - value : 초기에 노출되어야 할 입력값 (디폴트 값은 빈 문자열('')이다)
- 사용자가 입력한 값 유효성 검사
  - maxByte 값을 초과하면 maxByte 값까지 잘라서 표시

  maxByte 정보를 외부에서 받아와서 100바이트든 50바이트든 제한을 둘 수 있고, 이 컴포넌트 내부에서 maxByte 정보를 알고 있으니 유효성 검사를 하여 사용자가 입력한 값이 maxByte 값을 초과하면 maxByte 값까지 잘라서 표시할 것이다.

부모 컴포넌트에는 LimitByteInput 컴포넌트를 아래와 같이 사용할 것이다.

<template>
...
  <limit-byte-input
    :maxByte="100"
    v-model="alarmGroupDesc"
  />
</template>

<script>
export default {
  ...
  data() {
    return {
      ...
      alarmGroupDesc: '',
    }
  },
  ...
}
</script>

  LimitByteInput 컴포넌트 템플릿에는 사용자에게 글자를 입력받을 input 요소를 정의한다.

<input
  type="text"
  :value="value"
  @input="changeValue"
/>

  Vue에서는 한국어, 일본어, 중국어 등 IME가 요구되는 언어는 글자를 조합하는 중에 v-model이 업데이트되지 않기 때문에 v-model을 사용하는 대신 input 이벤트와 value 바인딩을 사용하라고 가이드 한다. 👀

input 요소에 maxlength 속성을 지정할 수 있지만 여기서는 사용하지 않았다. maxlength 속성은 입력할 수 있는 최대 문자열의 갯수를 정한다.

export default {
  props: {
    maxByte: {type: Number, default: 100},
    value: {type: String, default: ''},
  },
  methods: {
    changeValue(ev) {
      const { value } = ev.target
      const isValidByte = getByte(value) <= this.maxByte;     
      const validValue = isValidByte // 유효한 값인지 확인
        ? value // 유효하면 value 그대로 유효한 값이 됨
        : getLimitedByteText(value, this.maxByte) // value를 100바이트까지 계산해 잘린 값
        
      this.$emit('input', validValue)
       
      if(!isValidByte) {
        this.$forceUpdate()
      }
    },
  },
}

  changeValue 메소드에서 input 요소에서 입력받은 값을 유효한 값인지 확인한 후 유효하면 사용자가 입력한 값으로 유효한 값이 아니라면 100바이트까지 잘라서 유효한 값으로 만들어준다. this.$emit('input', validValue)를 사용해 부모 컴포넌트의 값을 업데이트한다. 이 때, 100바이트까지 잘라서 만들어진 값은 부모 컴포넌트에 있는 값과 같기 때문에 자식 컴포넌트는 리렌더링하지 않는다. this.$forceUpdate()를 사용해 input 요소에 100 바이트 길이까지 표시하였다.

limit-100byte-input(3)

정리하며...

  이렇게 Vue에서 입력된 글자의 바이트 수를 제한하는 컴포넌트를 만들어 보았다. 1) 한글같은 IME 지원을 하기 위해 input 요소에 v-model 대신 @input 이벤트와 v-bind:value를 사용하였으며, 2) 부모 컴포넌트에서 v-model로 값을 넘겨주고 자식 컴포넌트의 input 요소에 바인딩한 다음, 3) 사용자의 입력을 받아 값을 업데이트하고, 4) $emit('input', validValue)를 사용해 부모의 값을 업데이트하는 동작을 해보았다. 슬프게도 이 글을 배포하고 있는 시점에는 스펙이 100글자 제한으로 바뀌었다.😂 간단한 스펙이었지만 대학교 컴공 시절 주입식으로 받아들였던 비트 및 바이트 그리고 텍스트 인코딩에 대해서 톺아보고 한 땀 한 땀 코드로 작성해보는데 의미를 두겠다. 긴 코로나 시국으로 지칠 수도 있겠지만 즐겁게 코딩😝하길 바라며 이 글을 마치겠다. 🤗

참고

조정은2022.03.18
Back to list