[JS] Array.from 과 split 의 차이

yongkini ·2024년 10월 31일
1

JS

목록 보기
3/3

Array.from 과 split 의 차이

계기

: 일단 갑자기 이 부분에 대해서 포스팅을 하게된 계기는 우연히 다른 개발자분의 포스팅을 보게되면서 흥미가 생겨서 이다. 이분의 벨로그에서는 모바일 청첩장 관련 서비스를 직접 만드신 뒤에 생겼던 이슈를 공유하는 형태였는데, 재밌게 읽으면서 나도 직접 알아보고 싶어서 이 포스팅을 하게 됐다.

Array.from 과 Split? 무슨 관계일까? 🧐

: 개인적인 경험으로 split 같은 경우에는 서버에서 혹은 관리하는 데이터가 abc;def;ghi;jkl 이런식일때 ; 를 기준으로 데이터를 split하고 싶으면 쓰고 그랬던 것 같다.

const data = 'abc;def;ghi;jkl';
console.log(data.split(';')) // ['abc', 'def', 'ghi', 'jkl'];

: Array.from은 요즘 백엔드 API가 완성되기 전에 더미 데이터를 만들거나 static한 데이터를 매번 임의로 생성해서 뭔가를 렌더링해줘야할 때 썼던 것 같다. 혹은 간단하게 ArrayLike data를 배열로 만들어줄 때 썼다.

: 사실, 둘의 관계가 뭔가라는 것보다는 둘의 차이가 뭐냐는 말이 맞을 것 같다. 레퍼런스로 첨부한 개발자분은 이모티콘을 String으로 받아서(유저에게서 받으신 것 같다 input으로), 이걸 split하고, 이 배열을 통해서 어떤 로직을 처리하시려고 했던 것 같은데, 이 과정에서 생각지 못한 상황이 발생했다는 것.

const emojistring = "😊😜🤩😀" // 유저에게 이렇게 4개의 이모지 인풋을 받았고,
const splitEmojis = emojistring.split('') // 이 이모지들을 각각 별개로 배열에 담아두려고 한다.
console.log(splitEmojis); // 그 결과는 ? 
// ['\uD83D', '\uDE0A', '\uD83D', '\uDE1C', '\uD83E', '\uDD29', '\uD83D', '\uDE00'] ??

결론적으로 나온 데이터는 ['\uD83D', '\uDE0A', '\uD83D', '\uDE1C', '\uD83E', '\uDD29', '\uD83D', '\uDE00'] 이렇게 된다. 이게 뭘까?... 분명 우리가 기대했던건 ,이런 형태였을 것이다. ['😊', '😜', '🤩', '😀']

둘의 차이는 무엇이며 왜 split은 안되고, Array.from은 될까?

const emojistring = "😊😜🤩😀"
Array.from(emojistring); // ['😊', '😜', '🤩', '😀']

일단 결론적으로 이렇게 해주면 잘된다. 그리고 결론적으로 둘의 차이는 다음과 같다.

Array.from()이 동작하는 방식

Array.from()코드 포인트 단위로 작동합니다. [Symbol.iterator]라는 특수한 메서드를 통해 문자열을 순회하면서 유니코드 문자 하나(코드 포인트)를 하나의 문자로 인식하게 됩니다. 즉, 서로게이트 페어(Surrogate Pair)로 이루어진 문자도 하나의 코드 포인트로 처리할 수 있는 것이죠.

.split('')이 실패하는 이유

반면, .split('')은 문자열을 단순히 16비트 코드 유닛 단위로 나누기 때문에 서로게이트 페어를(Surrogate Pair) 하나의 문자로 인식하지 못합니다. 예를 들어, 😊 이모지를 .split('')으로 나누면 \uD83D\uDE0A로 나뉘게 되어 불완전한 문자로 처리됩니다.

둘의 차이의 포인트는 서로 동작하는 단위가 다르다는 것이다. Array.from은 코드 포인트 단위로 동작하고, split은 코드 유닛 단위로 동작한다.

Code Point 그리고 Code Unit

: 우리가 컴퓨터에게 어떤 입력을 주면 예를 들어, "안녕하세요"라는 입력이 들어오면 X라는 로직을 돌려줘. 라는 명령을 내렸다고 해보자. 그럼 컴퓨터는 일단 뒤쪽은 차치하고, "안녕하세요"라는 한국말을 어떻게 해석할 수 있을까? 우리는 컴퓨터가 0,1 밖에 못알아듣는다는 말을 자주 들어왔는데, 따라서 이들은 한국말을 해석할 때도 결국 0,1로 해석해야한다. 따라서, 우리가 입력하는 문자열 등도 결국은 이진수로 바꿔야한다. 그리고 이 때, 코드 포인트, 코드 유닛 개념이 등장한다.

  1. 😊 이모지는 유니코드 코드 포인트로 *U+1F60A입니다.
  2. UTF-16에서 이 코드 포인트를 저장하려면 서로게이트 페어(두 개의 코드 유닛)로 변환해야 하므로:
    • U+1F60A\uD83D (고차 서로게이트)와 \uDE0A (저차 서로게이트)라는 코드 유닛 두 개로 표현됩니다.

1) 컴퓨터가 이해할 수 있게 하려면 이진수로 표현해야한다.
2) 유니코드 표는 인간의 언어 우리가 쓰는 다양한 언어, 특수문자 등을 하나의 코드와 매핑 해놓은 것이다. 컴퓨터가 쓰는 사전으로 생각해봐도 좋을 것 같다. 예를 들어,

A-> \u0041
Apple -> \u0041\u0070\u0070\u006c\u0065

그리고 이를 코드 포인트(Code Point) 라고 한다(유니코드는 코드 포인트라는 하나의 시스템의 예시이다).
참고 : https://developer.mozilla.org/ko/docs/Glossary/Code_point

함수로도 만들 수 있다.

function stringToUnicode(inputString) {
  const unicodeArray = [];
  
  for (const char of inputString) {
    const codePoint = char.codePointAt(0).toString(16).toUpperCase();
    unicodeArray.push(`U+${codePoint.padStart(4, '0')}`);
  }
  
  return unicodeArray.join(' ');
}

// 사용 예시
console.log(stringToUnicode("Apple"));  // U+0041 U+0070 U+0070 U+006C U+0065

3) 일단 Apple이란 단어를 받았으면, 유니코드(코드 포인트)인 U+0041 U+0070 U+0070 U+006C U+0065 얻어낸다(사전을 통해 확인하듯이).

4) 그 다음엔 이를 코드 유닛 단위로 쪼개서 해석해보자. 사실 여기서는 따로 건들게 없다. U+0041 U+0070 U+0070 U+006C U+0065 여기서는 각각의 코드 포인트가 결국 코드 유닛이 된다. 여기서 코드 포인트를 코드 유닛으로 인코딩하는 일련의 규칙이 있는데 이를 UTF 라고 하고 여기선 UTF-16이 쓰였다.

여기서도 함수로 알아보자. 코드 포인트(유니코드) -> UTF-16 룰 적용 -> 코드 유닛 변환

function stringToCodeUnits(inputString) {
  const codeUnitsArray = [];

  for (const char of inputString) {
    // 1. 각 문자의 코드 포인트 구하기
    const codePoint = char.codePointAt(0);
    const codePointHex = `U+${codePoint.toString(16).toUpperCase().padStart(4, '0')}`;
    
    // 2. 코드 포인트를 UTF-16 코드 유닛으로 변환하기
    if (codePoint <= 0xFFFF) {
      // BMP 범위의 코드 포인트는 단일 코드 유닛으로 저장
      codeUnitsArray.push(codePointHex + ` (${codePoint.toString(16).toUpperCase()})`);
    } else {
      // BMP 바깥의 코드 포인트는 서로게이트 페어로 저장
      const adjustedCodePoint = codePoint - 0x10000;
      const highSurrogate = 0xD800 + (adjustedCodePoint >> 10);
      const lowSurrogate = 0xDC00 + (adjustedCodePoint & 0x3FF);
      codeUnitsArray.push(
        `${codePointHex} (High Surrogate: ${highSurrogate.toString(16).toUpperCase()}, Low Surrogate: ${lowSurrogate.toString(16).toUpperCase()})`
      );
    }
  }

  return codeUnitsArray.join(' ');
}

// 사용 예시
console.log(stringToCodeUnits("Apple"));  
// U+0041 (41) U+0070 (70) U+0070 (70) U+006C (6C) U+0065 (65)
console.log(stringToCodeUnits("😊"));
// U+1F60A (High Surrogate: D83D, Low Surrogate: DE0A)

**
Apple -> 코드 포인트: U+0041 -> 코드유닛: U+0070 (70) U+0070 (70) U+006C (6C) U+0065 (65)
😊 -> 코드 포인트: U+1F60A -> 코드 유닛: (High Surrogate: D83D, Low Surrogate: DE0A)

UTF-16의 코드 포인트 → 코드 유닛 변환 규칙
  • BMP (Basic Multilingual Plane) 내의 코드 포인트 (U+0000 ~ U+FFFF)

    이 범위의 코드 포인트는 단일 16비트 코드 유닛으로 표현됩니다. 예를 들어, U+0041(A)는 그대로 16비트 코드 유닛 0x0041로 저장됩니다.
    이 범위의 대부분의 문자(영어, 한글 등)가 하나의 코드 유닛으로 표현됩니다.

  • BMP 바깥의 코드 포인트 (U+10000 이상)

    U+10000 이상의 코드 포인트는 서로게이트 페어(surrogate pair)라는 두 개의 16비트 코드 유닛으로 표현됩니다.
    UTF-16은 이 범위의 문자를 표현할 때 고차 서로게이트(High Surrogate)와 저차 서로게이트(Low Surrogate) 두 개의 16비트 코드 유닛을 사용하여 하나의 유니코드 문자를 나타냅니다. 예를 들어, 😊(U+1F60A)는 \uD83D (고차 서로게이트)와 \uDE0A (저차 서로게이트)로 변환됩니다. 😊(U+1F60A) = \uD83D + \uDE0A

쉽게 말해, 보통 영어, 숫자, 한글 같은 유니코드 문자는 16비트 안에서 표현이 된다. 즉, 16 비트 크기만 써도 표현이 가능하다. 하지만, 이모지 같은 특수 문자들은(혹은 고대 문자, 중국어, 음표, 대문자 수학 기호 등) 16비트로 표현이 불가능하며 4바이트(2개의 16비트)가 필요하다.

다시 돌아와서, 왜 Array.from 은 되고, split은 안될까?

: 아까 말했듯이, Array.from은 코드 포인트 단위로 동작한다.

Apple -> 코드 포인트: U+0041 -> 코드유닛: U+0070 (70) U+0070 (70) U+006C (6C) U+0065 (65)
😊 -> 코드 포인트: U+1F60A -> 코드 유닛: (High Surrogate: D83D, Low Surrogate: DE0A)

이부분으로 다시 생각해보면, Array.from은 코드 포인트 단위로 계산하기 때문에 분리된 서로 게이트를 합쳐서 계산한다. 하지만, split은 코드 유닛 단위로 계산하기 때문에 이모지의 경우 분리된 서로게이트를 분리된 단위로 계산한다. 이에 따라

const emojistring = "😊😜🤩😀" // 유저에게 이렇게 4개의 이모지 인풋을 받았고,
const splitEmojis = emojistring.split('') // 이 이모지들을 각각 별개로 배열에 담아두려고 한다.
console.log(splitEmojis); // 그 결과는 ? 
// ['\uD83D', '\uDE0A', '\uD83D', '\uDE1C', '\uD83E', '\uDD29', '\uD83D', '\uDE00'] ??

위와 같이 4개의 이모지를 넣었으나, 8개의 요소를 가진 배열을 리턴하게 되는 것이다.

const notEmojistring = "Apple" 
const splitArr = notEmojistring.split('') 
console.log(splitArr); // ['A', 'p', 'p', 'l', 'e']

이 때 위와 같이 해주면, 정상적으로 출력이 된다. 이건 결론적으로, 코드 단위로 스플릿을 해도 16비트 내로 표현이 가능해서 서로게이트 페어로 나뉜 경우가 아니라면 정상적으로 데이터를 인식한다는 의미이고, 이모지가 같은 서로게이트 페어로 나뉜 경우 (High Surrogate: D83D, Low Surrogate: DE0A) 이 각각의 서로게이트를 따로 해석하려다보니(원래는 둘이 합쳐져야 의미가 있는 것인데..) 이런식으로 원하지 않는 결말(?)이 나오게 된 것이다. 그래서 이렇게

U+10000 이상의 코드 포인트는 서로게이트 페어(surrogate pair)라는 두 개의 16비트 코드 유닛으로 표현됩니다.

표현되는 문자들(예를 들어, 𝄞, 𝒜, 𓀀, 𠀀, 🐱 etc)을 가지고 컴퓨터에게 뭔가를 시킬 때는 위와 같은 엔코딩 과정과 디코딩 관계를 거쳐서 저장과 해석이 된다는 점을 생각하면서 프로그래밍 해야한다. 그렇지 않으면 알 수 없는 결과를 마주하게 된다!.

결론

: 프론트엔드 개발을 하다보면, 한국말이나 특수한 문자들을 서버에 HTTP Request로 보내는 경우 등에도 간혹 글자가 깨지는 등의 문제가 발생할 때가 많은데, 이것과 관련해서도 결국 컴퓨터가 해석할 수 있는 언어로 문자를 엔코딩하는 과정에서 따르는 규칙간의 씽크가 맞지 않았다던지 하는 이슈가 있는 걸로 예상해볼 수 있다. 이처럼, 문자열 데이터를 보여줘야하는 일이 허다한(?) FE 개발에 있어서 컴퓨터가 데이터를 저장 및 파싱하는 방법과 그걸 위해서 우리가 준수해야하는 규칙들(유니코드, UTF 등) 그리고 그 규칙들을 해석하는 그리고 참조하는 규칙이 다른 케이스들로 인해 생기는 현상들을 충분히 이해해야 이런 버그들을 이해하고 해결할 수 있을 것 같다.

profile
완벽함 보다는 최선의 결과를 위해 끊임없이 노력하는 개발자

0개의 댓글