[모던 자바스크립트 튜토리얼] 5.3 문자열

개발견 배도르만·2023년 3월 31일
0
post-thumbnail

문자열

텍스트 형식의 데이터는 문자열에 저장된다. 다른 언어에서 '문자'형(1글자)과 '문자열'형(1글자 이상)을 구분하는 경우가 있는데 자바스크립트는 모두 '문자열' 형태로 저장한다.
또한 자바스크립트의 문자열은 페이지 인코딩 방식에 상관없이 항상 UTF-16 형식을 따른다.

따옴표

문자열은 작은따옴표, 큰따옴표, 백틱으로 감쌀 수 있다.

let single = '작은따옴표';//둘은 기능상 차이 없음
let double = "큰따옴표";//둘은 기능상 차이 없음

let backticks = `백틱`;//특별한 기능 포함

백틱은 백틱 내에 ${}로 감싸는 방법으로 표현식을 사용할 수 있다(템플릿 리터럴).

function sum(a, b) {
  return a + b;
}

alert(`1 + 2 = ${sum(1, 2)}.`); // 1 + 2 = 3.

또한 여러 줄에 걸쳐 문자열을 작성할 수도 있다.

let guestList = `손님:
 * John
 * Pete
 * Mary
`;

alert(guestList); // 손님 리스트를 여러 줄에 걸쳐 작성함

특수 기호

'\'(backslash character)은 이스케이프 문자(escape character)로도 불리며, 백슬래시로 시작하는 다양한 특수 문자가 있다.

이스케이프 문자는 특정 문자(', ", \ 등)의 기능을 피해 문자 그대로 읽기 위한 용도로 사용된다.

줄 바꿈 문자(newline character)

'\n'을 사용하여 줄바꿈 기능을 수행할 수 있다.

let str1 = "Hello\nWorld"; // '줄 바꿈 기호'를 사용해 두 줄짜리 문자열을 만듦

// 백틱과 일반적인 줄 바꿈 방법(엔터)을 사용해 두 줄짜리 문자열을 만듦
let str2 = `Hello
World`;

alert(str1 == str2); // true

이외에도 다양한 특수 문자가 있다.

alert( 'I\'m the Walrus!' ); // I'm the Walrus!
alert( `역슬래시: \\` ); // 역슬래시: \

문자열의 길이

문자열의 'length' 프로퍼티에는 문자열의 길이가 저장된다.

alert( `My\n`.length ); // 3
/* 함수가 아닌 프로퍼티이기에 소괄호가 붙지 않는다. */

특정 글자에 접근하기

문자열의 특정 위치(n 번째)에 접근하기 위해서는 대괄호 []를 사용하거나 str.charAt() 메서드를 사용한다.

let str = `Hello`;

// 첫 번째 글자
alert( str[0] ); // H
alert( str.charAt(0) ); // H

// 마지막 글자
alert( str[str.length - 1] ); // o

두 메서드의 차이점은 반환할 글자가 없을 때의 반환값이다.
[]는 undefined를
charAt()는 빈 문자열을 반환한다.

for..of 를 사용하여 문자열의 글자를 대상으로 반복 작업을 할 수 있다.

for (let char of "Hello") {
  alert(char); // H,e,l,l,o (char는 순차적으로 H, e, l, l, o가 됩니다.)
}

문자열의 불변성

문자열은 수정할 수 없기 때문에 재할당하는 방식을 사용한다.

let str = 'Hi';

str[0] = 'h'; // Error: Cannot assign to read only property '0' of string 'Hi'
alert( str[0] ); // 동작하지 않습니다.

대소문자 변경

toLowerCase() - 문자열을 소문자로 변경
toUpperCase() - 문자열을 대문자로 변경

alert( 'Interface'.toUpperCase() ); // INTERFACE
alert( 'Interface'.toLowerCase() ); // interface

alert( 'Interface'[0].toLowerCase() ); // 'i'

부분 문자열 찾기

str.indexOf(substr, pos)

문자열 str의 pos로부터 부분 문자열 substr이 어디에 위치하는지 찾아준다. 찾은 문자열의 위치를 반환하고, 없으면 -1을 반환한다.

pos는 필수 매개변수가 아니다. pos를 인자로 넘기지 않으면 0번째 글자부터 찾는다.

let str = 'Widget with id';

alert( str.indexOf('Widget') ); // 0, str은 'Widget'으로 시작함
alert( str.indexOf('widget') ); // -1, indexOf는 대·소문자를 따지므로 원하는 문자열을 찾지 못함

alert( str.indexOf("id") ); // 1, "id"는 첫 번째 위치에서 발견됨 (Widget에서 id)
alert( str.indexOf('id', 2) ) // 12

문자열 끝에서 반대로 찾는 함수는 str.lastIndexOf(substr, position)이다.

indexOf() 사용 시 주의할 점은 반환값이 0일 경우에 if문에서 false로 처리된다는 것이다.

let str = "Widget with id";

if (str.indexOf("Widget")) {
    alert("찾았다!"); // 의도한 대로 동작하지 않습니다.
}

따라서 조건부를 -1과 비교하는 방식으로 작성한다.

let str = "Widget with id";

if (str.indexOf("Widget") != -1) {
    alert("찾았다!"); // 의도한 대로 동작합니다.
}

비트 NOT 연산자를 사용한 기법

비트 NOT 연산자 '~'를 사용하면 결과적으로 부호가 반전되고 1이 더해진 수가 반환된다.(~n == n+1)
indexOf()로 선택한 문자열을 찾지 못했을 때 반환되는 수는 -1이므로, ~연산자를 사용하면 0이 반환된다(4294967295 미만의 수에서만). 따라서 ~str.indexOf()로 더 짧은 코드를 작성할 수 있다.

let str = "Widget";

if (~str.indexOf("Widget")) {
  alert( '찾았다!' ); // 의도한 대로 동작합니다.
}

includes

문자열 위치와 관계 없이 포함 여부만 알고 싶은 경우 사용

alert( "Widget".includes("id") ); // true
alert( "Widget".includes("abc") ); // false
alert( "Widget".includes("id", 3) ); // false, 세 번째 위치 이후엔 "id"가 없습니다.

startWith, endWith

문자열이 특정 문자열로 시작하거나 끝나는지 여부를 확인할 때 사용

alert( "Widget".startsWith("Wid") ); // true, "Widget"은 "Wid"로 시작합니다.
alert( "Widget".endsWith("get") ); // true, "Widget"은 "get"으로 끝납니다.

부분 문자열 추출하기

slice(start [, end])

문자열의 start부터 end까지 반환(end는 미포함)

let str = "stringify";
alert( str.slice(0, 5) ); // 'strin', 0번째부터 5번째 위치까지(5번째 위치의 글자는 포함하지 않음)
alert( str.slice(0, 1) ); // 's', 0번째부터 1번째 위치까지(1번째 위치의 자는 포함하지 않음)
alert( str.slice(2) ); // ringify, 2번째부터 끝까지
alert( str.slice(-4, -1) ); // gif

str.substring(start [, end])

start와 end 사이에 있는 문자열을 반환

substring과의 차이는 다음 예시와 같다.

let str = "stringify";

// 동일한 부분 문자열을 반환합니다.
alert( str.substring(2, 6) ); // "ring"
alert( str.substring(6, 2) ); // "ring"

// slice를 사용하면 결과가 다릅니다.
alert( str.slice(2, 6) ); // "ring" (같음)
alert( str.slice(6, 2) ); // "" (빈 문자열)

substring은 음수 인수를 허용하지 않고, 0으로 처리한다.

str.substr(start [, length])

start에서부터 시작해 length 개의 글자를 반환

let str = "stringify";
alert( str.substr(2, 4) ); // ring, 두 번째부터 글자 네 개
첫 번째 인수가 음수면 뒤에서부터 개수를 셉니다.

let str = "stringify";
alert( str.substr(-4, 2) ); // gi, 끝에서 네 번째 위치부터 글자 두 개

substr는 코어 자바스크립트 명세서가 아닌, 구식 스크립트에 대응하기 위해 남겨 둔 브라우저 전용 기능들을 명시해 놓은 부록 B(Annex B)에 정의되어있다. 거의 모든 곳에서 이 메서드가 동작하긴 하지만 브라우저 이외의 호스트 환경에서는 제대로 동작하지 않을 수 있다.

따라서 확장성과 호환성을 고려한다면 substr 대신 substring, slice를 사용해야 할 것인데, substring은 음수를 허용하지 않기 때문에 slice가 좀 더 유연하다. slice만 알고 사용해도 부분 추출에서의 문제는 없을 것이다.

문자열 비교하기

문자열을 비교할 땐 알파벳 순서를 기준으로 글자끼리 비교가 이뤄진다.

특이사항

  • 소문자는 대문자보다 항상 크다.
alert( 'a' > 'Z' ); // true
  • 발음 구별 기호(diacritical mark)가 붙은 문자는 알파벳 순서 기준을 따르지 않는다.
alert( 'Österreich' > 'Zealand' ); // true (Österreich는 오스트리아를 독일어로 표기한 것)

이런 예외사항 때문에 이름순으로 국가를 나열할 때 예상치 못한 결과가 나올 수 있다(Österreich가 Zealand보다 뒤에 나옴).

자바스크립트에서 모든 문자열은 UTF-16을 사용해 인코딩되는데, UTF-16에선 모든 글자가 숫자 형식의 코드와 매칭된다.

str.codePointAt(pos)

pos에 위치한 글자의 코드를 반환한다.

// 글자는 같지만 케이스는 다르므로 반환되는 코드가 다릅니다.
alert( "z".codePointAt(0) ); // 122
alert( "Z".codePointAt(0) ); // 90

String.fromCodePoint(code)

숫자 형식의 code에 대응하는 글자를 만들어준다.

alert( String.fromCodePoint(90) ); // Z
\u 뒤에 특정 글자에 대응하는 16진수 코드를 붙이는 방식으로도 원하는 글자를 만들 수 있습니다.

// 90을 16진수로 변환하면 5a입니다.
alert( '\u005a' ); // Z

코드 65와 220 사이(라틴계열 알파벳과 기타 글자들이 여기에 포함됨)에 대응하는 글자들을 출력하면

let str = '';

for (let i = 65; i <= 220; i++) {
  str += String.fromCodePoint(i);
}
alert( str );
// ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~€‚ƒ„
// ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜ

대문자 알파벳이 가장 먼저 나오고 특수 문자 몇 개가 나온 다음에 소문자 알파벳이 나온다. Ö은 거의 마지막에 출력된다.

a(코드97) > Z(코드90)인 이유이다. 순서가 그렇다.

문자열 제대로 비교하기

언어마다 문자 체계가 달라 문자열을 ‘제대로’ 비교하는 것은 간단하지 않다.

일단 페이지에서 어떤 언어를 사용하고 있는지 브라우저가 알아야 한다.

모던 브라우저 대부분이 국제화 관련 표준인 ECMA-402를 지원한다.
ECMA-402엔 언어가 다를 때 적용할 수 있는 문자열 비교 규칙과 이를 준수하는 메서드가 정의되어있다.

localeCompare()

ECMA-402에서 정의한 규칙에 따라 대상 문자열이 인자로 넘긴 문자열보다 작은지, 같은지, 큰지를 나타내주는 정수가 반환된다.

str.localeCopare(str2) // str을 str2에 비교

str이 str2보다 작으면 음수를 반환합니다.
str이 str2보다 크면 양수를 반환합니다.
str과 str2이 같으면 0을 반환합니다.

문자열 심화

이모티콘이나 일부 수학 기호, 상형 문자를 비롯한 희귀 기호 등을 다뤄야 한다면 심화적인 내용을 알아야 한다.

서로게이트 쌍

자주 사용되는 글자들은 모두 2바이트 코드를 가지고 있다(유럽권 언어에서 사용되는 글자, 숫자, 상형 문자 대다수).

그런데 2바이트는 65,536(2의 16승)개의 조합밖에 만들어내지 못하기 때문에 현존하는 기호를 모두 표현하기에 충분하지 않다. 이를 극복하기 위해 사용 빈도가 낮은 기호는 '서로게이트 쌍(surrogate pair)'이라 불리는 2바이트 글자들의 쌍을 사용해 인코딩한다.

서로게이트 쌍을 사용해 인코딩한 기호의 길이는 2이다.

alert( '𝒳'.length ); // 2, 수학에서 쓰이는 대문자 X(그리스 문자 카이 - 옮긴이)
alert( '😂'.length ); // 2, 웃으면서 눈물 흘리는 얼굴을 나타내는 이모티콘
alert( '𩷶'.length ); // 2, 사용 빈도가 낮은 중국어(상형문자)

자바스크립트가 만들어졌을 당시엔 서로게이트 쌍은 존재하지 않았습니다. 따라서 자바스크립트는 서로게이트 쌍으로 표현한 기호를 제대로 처리하지 못합니다.

위 예시에서 기호는 하나지만 길이는 2인 이유는 자바스크립트가 만들어졌을 당시에는 서로게이트 쌍이 존재하지 않았고, 따라서 자바스크립트는 해당 기호를 제대로 처리하지 못한다.
해당 기호들을 제대로 처리할 수 있는 몇 안 되는 메서드가 앞서 살펴본 String.fromCodePoint와 str.codePointAt이다.
두 메서드 등장 이전엔 String.fromCharCode와 str.charCodeAt을 사용했는데, 동일하게 동작하지만 서로게이트 쌍을 처리하진 못한다.

String.fromCodePoint와 str.codePointAt은 명세서에 추가된 지 얼마 안 된 메서드로, 서로게이트 쌍을 제대로 처리할 수 있는 몇 안 되는 메서드 입니다. 두 메서드가 등장하기 전에는 String.fromCharCode와 str.charCodeAt을 사용했었는데, 이 메서드들은 fromCodePoint, codePointAt과 동일하게 동작하지만 서로게이트 쌍은 처리하지 못합니다.

서로게이트 쌍을 구성하는 글자들은 붙어있을 때만 의미가 있다

alert( '𝒳'[0] ); // 이상한 기호가 출력됨
alert( '𝒳'[1] ); // 서로게이트 쌍의 일부가 출력됨

서로게이트 쌍을 다루는 다양한 방법에 대해선 iterable 객체 챕터에서 살펴볼 것이다.

발음 구별 기호와 유니코드 정규화

여러 언어에서 베이스가 되는 글자 위나 아래에 발음 구별 기호라 불리는 기호를 붙여 글자를 만든다.
예시) àáâäãåā

이런 ‘합성’ 글자 대부분은 UTF-16 테이블에서 독자적인 코드를 갖는다. 그런데 모든 합성 글자에 코드가 부여되지는 않는다. 조합 가능한 글자의 수가 너무 많기 때문이다.

임의의 조합을 지원하기 위해 UTF-16에선 몇 개의 유니코드 문자를 남겨두었다. 베이스 글자 뒤에 하나 혹은 여러 개의 유니코드 문자를 붙여 베이스 글자를 꾸밀 수 있도록 하기 위함이다.

예시) 베이스 글자 S 뒤에 '윗 점’을 나타내는 유니코드 문자(\u0307)를 붙여 Ṡ를 만들 수 있다.

alert( 'S\u0307' ); // Ṡ

추가 발음 구별 기호가 필요한 경우 연속으로 사용 가능하다.

alert( 'S\u0307\u0323' ); // Ṩ

유연한 방식이지만, 단점도 있다. 눈으로 봤을 때는 같은 글자인데 유니코드 조합이 다른 경우가 생기는 것이다.

let s1 = 'S\u0307\u0323'; // Ṩ, S + 윗 점 + 아랫 점
let s2 = 'S\u0323\u0307'; // Ṩ, S + 아랫 점 + 윗 점

alert( `s1: ${s1}, s2: ${s2}` );

alert( s1 == s2 ); // 눈으로 보기엔 같은 글자이지만 동등 비교 시 false가 반환됩니다.

이런 문제를 해결하려면 '유니코드 정규화(unicode normalization)'라 불리는 알고리즘을 사용해 각 문자열을 동일한 형태로 '정규화’해야 한다.

이는 str.normalize()에 구현되어 있다.

alert( "S\u0307\u0323".normalize() == "S\u0323\u0307".normalize() ); // true

S 위, 아래에 점을 붙이는 사례에선 normalize()를 사용하면 세 개의 글자가 하나로 합쳐진다(Ṩ를 나타내는 유니코드 \u1e68).

alert( "S\u0307\u0323".normalize().length ); // 1

alert( "S\u0307\u0323".normalize() == "\u1e68" ); // true

그런데 항상 이렇지는 않다. Ṩ는 UTF-16 테이블에 포함되어 있기 때문에 정규화가 되는 것이다. 포함되어 있지 않은 문자로는 정규화되지 않는다.

✍️ 정리

  • 따옴표로 문자열 표현 가능
  • 큰따옴표와 작은따옴표는 기능이 같다.
  • 백틱 사용 시 문자열을 여러 줄에 걸쳐 쓸 수 있다. 또한 문자열 내에 ${…}을 사용해 표현식도 넣을 수 있다.
  • 자바스크립트에선 UTF-16을 사용해 문자열을 인코딩한다.
  • \n 같은 특수 문자를 사용할 수 있다.
  • \u...를 사용하면 해당 문자의 유니코드를 사용해 글자를 만들 수 있다.
  • 문자열 내의 글자 하나를 얻으려면 대괄호 []를 사용한다.
  • 부분 문자열을 얻으려면 slice나 substring을 사용한다.
  • 소문자로 바꾸려면 toLowerCase, 대문자로 바꾸려면 toUpperCase를 사용한다.
  • indexOf를 사용하면 부분 문자열의 위치를 얻을 수 있다.
  • 부분 문자열 여부만 알고 싶다면 includes/startsWith/endsWith를 사용한다.
  • 특정 언어에 적합한 비교 기준 사용해 문자열을 비교하려면 localeCompare를 사용한다.
  • 이외에도 문자열에 쓸 수 있는 유용한 메서드
    • str.trim() – 문자열 앞과 끝의 공백 문자를 제거
    • str.repeat(n) – 문자열을 n번 반복
  • 정규표현식은 큰 주제이기에 별도의 주제로 다룬다.
profile
네 발 개발 개

0개의 댓글