📢 22/05/30 복습
https://ko.javascript.info/string
참고 사이트에 내용을 개인적으로 복습하기 편하도록 재구성한 글입니다.
자세한 설명은 참고 사이트를 살펴보시기 바랍니다.
자바스크립트엔 글자 하나만 저장할 수 있는 별도의 자료형이 없습니다. 텍스트 형식의 데이터는 길이에 상관없이 문자열 형태로 저장됩니다.
자바스크립트에서 문자열은 페이지 인코딩 방식과 상관없이 항상 UTF-16 형식을 따릅니다.
문자열은 작은따옴표나 큰따옴표, 백틱으로 감쌀 수 있습니다.
let single = '작은따옴표';
let double = "큰따옴표";
let backticks = `백틱`;
작은따옴표와 큰따옴표는 기능상 차이가 없습니다. 그런데 백틱엔 특별한 기능이 있습니다. 표현식을 ${...}
로 감싸고 이를 백틱으로 감싼 문자열 중간에 넣어주면 해당 표현식을 문자열 중간에 쉽게 삽입할 수 있죠. 이런 방식을 템플릿 리터럴(template literal)이라고 부릅니다.
function sum(a, b) {
return a + b;
}
alert(`1 + 2 = ${sum(1, 2)}.`); // 1 + 2 = 3.
백틱을 사용하면 문자열을 여러 줄에 걸쳐 작성할 수도 있습니다.
let guestList = `손님:
* John
* Pete
* Mary
`;
alert(guestList); // 손님 리스트를 여러 줄에 걸쳐 작성함
자연스럽게 여러 줄의 문자열이 만들어졌네요. 작은따옴표나 큰따옴표를 사용하면 위와 같은 방식으로 여러 줄짜리 문자열을 만들 수 없습니다.
let guestList = "손님: // Error: Invalid or unexpected token
* John";
작은따옴표나 큰따옴표로 문자열을 표현하는 방식은 자바스크립트가 만들어졌을 때부터 있었습니다. 이때는 문자열을 여러 줄에 걸쳐 작성할 생각조차 못 했던 시기였죠. 백틱은 그 이후에 등장한 문법이기 때문에 따옴표보다 다양한 기능을 제공합니다.
🔥 백틱은 "템플릿 함수(template function)"에서도 사용됩니다.
첫 번째 백틱 바로 앞에 함수 이름을 써주면, 이 함수는 백틱 안의 문자열 조각이나 표현식 평가 결과를 인수로 받아 자동으로 호출됩니다. 이런 기능을 "태그드 템플릿(tagged template)"이라 부르는데, 태그드 템플릿을 사용하면 사용자 지정 템플릿에 맞는 문자열을 쉽게 만들 수 있습니다. 참고로 이 기능은 자주 사용되진 않습니다.
const tag = function(strings, argu1, argu2) {
return (
`
strings: ${JSON.stringify(strings)}
argu1: ${argu1}
argu2: ${argu2}
`
)
}
const name = 'Tomas';
const age = 28;
alert(tag`Hi, My name is ${name}. I'm ${age} years old.`)
// strings: ["Hi, My name is",". I'm "," years old."]
// argu1: Tomas
// argu2: 28
"줄 바꿈 문자(newline character)"라 불리는 특수기호 \n
을 사용하면 작은따옴표나 큰따옴표로도 여러 줄 문자열을 만들 수 있습니다.
let guestList = "손님:\n * John\n * Pete\n * Mary";
alert(guestList); // 손님 리스트를 여러 줄에 걸쳐 작성함
따옴표를 이용해 만든 여러 줄 문자열과 백틱을 이용해 만든 여러 줄 문자열은 표현 방식만 다를 뿐 차이가 없습니다.
let str1 = "Hello\nWorld"; // "줄 바꿈 기호"를 사용해 두 줄짜리 문자열을 만듦
// 백틱과 일반적인 줄 바꿈 방법(엔터)을 사용해 두 줄짜리 문자열을 만듦
let str2 = `Hello
World`;
alert(str1 == str2); // true
자바스크립트엔 줄 바꿈 문자를 비롯한 다양한 "특수" 문자들이 있습니다.
모든 특수 문자는 "이스케이프 문자(escape character)"라고도 불리는 역슬래시 (backslash character) \
로 시작합니다.
역슬래시 \
는 문자열을 정확하게 읽기 위한 용도로 만들어졌으므로 \
는 제 역할이 끝나면 사라집니다. 메모리에 저장되는 문자열엔 \
가 없습니다.
특수 문자 | 설명 |
---|---|
\n | 줄 바꿈 |
\r | 캐리지 리턴(carriage return). Windows에선 캐리지 리턴과 줄 바꿈 특수 문자를 조합(\r\n)해 줄을 바꿉니다. 캐리지 리턴을 단독으론 사용하는 경우는 없습니다. |
\',\" | 따옴표 |
\\ | 역슬래시 |
\t | 탭 |
\b, \f, \v | 각각 백스페이스(Backspace), 폼 피드(Form Feed), 세로 탭(Vertical Tab)을 나타냅니다. 호환성 유지를 위해 남아있는 기호로 요즘엔 사용하지 않습니다. |
\xXX | 16진수 유니코드 XX로 표현한 유니코드 글자입니다(예시: 알파벳 "z"는 "\x7A"와 동일함). |
\uXXXX | UTF-16 인코딩 규칙을 사용하는 16진수 코드 XXXX로 표현한 유니코드 기호입니다. XXXX는 반드시 네 개의 16진수로 구성되어야 합니다(예시: \u00A9는 저작권 기호 ©의 유니코드임). |
\u{X...XXXXXX}(한 개에서 여섯 개 사이의 16진수 글자) | UTF-32로 표현한 유니코드 기호입니다. 몇몇 특수한 글자는 두 개의 유니코드 기호를 사용해 인코딩되므로 4바이트를 차지합니다. 이 방법을 사용하면 긴 코드를 삽입할 수 있습니다. |
alert( "\u00A9" ); // ©
alert( "\u{20331}" ); // 佫, 중국어(긴 유니코드)
alert( "\u{1F60D}" ); // 😍, 웃는 얼굴 기호(긴 유니코드)
length
프로퍼티엔 문자열의 길이가 저장됩니다.
alert( `My\n`.length ); // 3
\n
은 "특수 문자" 하나로 취급되기 때문에 My\n
의 길이는 3
입니다.
length
는 함수가 아니고, 숫자가 저장되는 프로퍼티라는 점에 주의하시기 바랍니다. 뒤에 괄호를 붙일 필요가 없습니다.
문자열 내 특정 위치인 pos
에 있는 글자에 접근하려면 [pos]
같이 대괄호를 이용하거나 str.charAt(pos)
라는 메서드를 호출하면 됩니다. 위치는 0
부터 시작합니다.
let str = `Hello`;
// 첫 번째 글자
alert( str[0] ); // H
alert( str.charAt(0) ); // H
// 마지막 글자
alert( str[str.length - 1] ); // o
근래에는 대괄호를 이용하는 방식을 사용합니다. charAt
은 하위 호환성을 위해 남아있는 메서드라고 생각하시면 됩니다.
두 접근 방식의 차이는 반환할 글자가 없을 때 드러납니다. 접근하려는 위치에 글자가 없는 경우 []
는 undefined
를, charAt
은 빈 문자열을 반환합니다.
let str = `Hello`;
alert( str[1000] ); // undefined
alert( str.charAt(1000) ); // "" (빈 문자열)
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] ); // 동작하지 않습니다.
이런 문제를 피하려면 완전히 새로운 문자열을 하나 만든 다음, 이 문자열을 str
에 할당하면 됩니다.
let str = "Hi";
str = "h" + str[1]; // 문자열 전체를 교체함
alert( str ); // hi
메서드 toLowerCase()
와 toUpperCase()
는 대문자를 소문자로, 소문자를 대문자로 변경(케이스 변경)시켜줍니다.
alert( "Interface".toUpperCase() ); // INTERFACE
alert( "Interface".toLowerCase() ); // interface
글자 하나의 케이스만 변경하는 것도 가능합니다.
alert( "Interface"[0].toLowerCase() ); // "i"
첫 번째 방법은 str.indexOf(substr, pos)
메서드를 이용하는 것입니다.
이 메서드는 문자열 str
의 pos
에서부터 시작해, 부분 문자열 substr
이 어디에 위치하는지를 찾아줍니다. 원하는 부분 문자열을 찾으면 위치를 반환하고 그렇지 않으면 -1
을 반환합니다.
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)
str.indexOf(substr, pos)
의 두 번째 매개변수 pos
는 선택적으로 사용할 수 있는데, 이를 명시하면 검색이 해당 위치부터 시작됩니다.
부분 문자열 "id"
는 위치 1
에서 처음 등장하는데, 두 번째 인수에 2
를 넘겨 "id"
가 두 번째로 등장하는 위치가 어디인지 알아봅시다.
let str = "Widget with id";
alert( str.indexOf("id", 2) ) // 12
문자열 내 부분 문자열 전체를 대상으로 무언가를 하고 싶다면 반복문 안에 indexOf
를 사용하면 됩니다. 반복문이 하나씩 돌 때마다 검색 시작 위치가 갱신되면서 indexOf
가 새롭게 호출됩니다.
let str = "As sly as a fox, as strong as an ox";
let target = "as";
let pos = -1;
while ((pos = str.indexOf(target, pos + 1)) != -1) {
alert( `위치: ${pos}` );
}
🔥 str.lastIndexOf(substr, position)
str.lastIndexOf(substr, position)
는 indexOf
와 유사한 기능을 하는 메서드입니다. 문자열 끝에서부터 부분 문자열을 찾는다는 점만 다릅니다.
반환되는 부분 문자열 위치는 문자열 끝이 기준입니다.
if
문의 조건식에 indexOf
를 쓸 때 주의할 점이 하나 있습니다. 아래와 같이 코드들 작성하면 원하는 결과를 얻을 수 없습니다.
let str = "Widget with id";
if (str.indexOf("Widget")) {
alert("찾았다!"); // 의도한 대로 동작하지 않습니다.
}
str.indexOf("Widget")
은 0
을 반환하는데, if
문에선 0
을 false
로 간주하므로 alert
창이 뜨지 않습니다.
따라서 부분 문자열 여부를 검사하려면 아래와 같이 -1
과 비교해야 합니다.
let str = "Widget with id";
if (str.indexOf("Widget") != -1) {
alert("찾았다!"); // 의도한 대로 동작합니다.
}
오래전부터 전해 오는 비트(bitwise) NOT 연산자 ~
를 사용한 기법 하나를 소개해드리겠습니다. 비트 NOT 연산자는 피연산자를 32비트 정수로 바꾼 후(소수부는 모두 버려짐) 모든 비트를 반전합니다.
따라서 n
이 32비트 정수일 때 ~n
은 -(n+1)
이 됩니다.
alert( ~2 ); // -3, -(2+1)과 같음
alert( ~1 ); // -2, -(1+1)과 같음
alert( ~0 ); // -1, -(0+1)과 같음
alert( ~-1 ); // 0, -(-1+1)과 같음
위 예시에서 본 바와 같이 부호가 있는 32비트 정수 n
중, ~n
을 0
으로 만드는 경우는 n == -1
일 때가 유일합니다.
이를 응용해서 indexOf
가 -1
을 반환하지 않는 경우를 if ( ~str.indexOf("...") )
로 검사해 봅시다.
이렇게 ~str.indexOf("...")
를 사용하면 코드의 길이를 줄일 수 있습니다.
let str = "Widget";
if (~str.indexOf("Widget")) {
alert( "찾았다!" ); // 의도한 대로 동작합니다.
}
사실 이렇게 언어 특유의 기능을 사용해 직관적이지 않은 코드를 작성하는 것을 추천해 드리진 않습니다. 그렇지만 위와 같은 기법은 오래된 스크립트에서 쉽게 만날 수 있기 때문에 알아두어야 합니다.
if (~str.indexOf(...))
패턴의 코드를 만나면 "부분 문자열인지 확인"하는 코드라고 기억해둡시다. 이런 기법은 오래된 자바스크립트에서만 볼 수 있습니다.
비교적 근래에 나온 메서드인 str.includes(substr, pos)
는 str
에 부분 문자열 substr
이 있는지에 따라 true
나 false
를 반환합니다.
부분 문자열의 위치 정보는 필요하지 않고 포함 여부만 알고 싶을 때 적합한 메서드입니다.
alert( "Widget with id".includes("Widget") ); // true
alert( "Hello".includes("Bye") ); // false
str.includes
에도 str.indexOf
처럼 두 번째 인수를 넘기면 해당 위치부터 부분 문자열을 검색합니다.
alert( "Widget".includes("id") ); // true
alert( "Widget".includes("id", 3) ); // false, 세 번째 위치 이후엔 "id"가 없습니다.
메서드 str.startsWith
와 str.endsWith
는 메서드 이름 그대로 문자열 str
이 특정 문자열로 시작하는지 여부와 특정 문자열로 끝나는지 여부를 확인할 때 사용할 수 있습니다.
alert( "Widget".startsWith("Wid") ); // true, "Widget"은 "Wid"로 시작합니다.
alert( "Widget".endsWith("get") ); // true, "Widget"은 "get"으로 끝납니다.
문자열의 start
부터 end
까지(end
는 미포함)를 반환합니다.
let str = "stringify";
alert( str.slice(0, 5) ); // "strin", 0번째부터 5번째 위치까지(5번째 위치의 글자는 포함하지 않음)
alert( str.slice(0, 1) ); // "s", 0번째부터 1번째 위치까지(1번째 위치의 자는 포함하지 않음)
두 번째 인수가 생략된 경우엔, 명시한 위치부터 문자열 끝까지를 반환합니다.
let str = "stringify";
alert( str.slice(2) ); // ringify, 2번째부터 끝까지
start
와 end
는 음수가 될 수도 있습니다. 음수를 넘기면 문자열 끝에서부터 카운팅을 시작합니다.
let str = "stringify";
// 끝에서 4번째부터 시작해 끝에서 1번째 위치까지
alert( str.slice(-4, -1) ); // gif
start
와 end
사이에 있는 문자열을 반환합니다.
substring
은 slice
와 아주 유사하지만 start
가 end
보다 커도 괜찮다는 데 차이가 있습니다.
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
으로 처리됩니다.
메서드 | 추출할 부분 문자열 | 음수 허용 여부(인수) |
---|---|---|
slice(start, end) | start부터 end까지(end는 미포함) | 음수 허용 |
substring(start, end) | start와 end 사이 | 음수는 0으로 취급함 |
문자열을 비교할 땐 알파벳 순서를 기준으로 글자끼리 비교가 이뤄집니다.
그런데 아래와 같이 몇 가지 이상해 보이는 것들이 있습니다.
소문자는 대문자보다 항상 큽니다.
alert( "a" > "Z" ); // true
발음 구별 기호(diacritical mark)가 붙은 문자는 알파벳 순서 기준을 따르지 않습니다.
alert( "Österreich" > "Zealand" ); // true (Österreich는 오스트리아를 독일어로 표기한 것임)
이런 예외사항 때문에 이름순으로 국가를 나열할 때 예상치 못한 결과가 나올 수 있습니다. 사람들은 Österreich
가 Zealand
보다 앞서 나올 것이라 예상하는데 그렇지 않죠.
자바스크립트 내부에서 문자열이 어떻게 표시되는지 상기하며 원인을 알아봅시다.
모든 문자열은 UTF-16을 사용해 인코딩되는데, UTF-16에선 모든 글자가 숫자 형식의 코드와 매칭됩니다. 코드로 글자를 얻거나 글자에서 연관 코드를 알아낼 수 있는 메서드는 다음과 같습니다.
pos
에 위치한 글자의 코드를 반환합니다.
// 글자는 같지만 케이스는 다르므로 반환되는 코드가 다릅니다.
alert( "z".codePointAt(0) ); // 122
alert( "Z".codePointAt(0) ); // 90
숫자 형식의 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{|}~
// ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜ
대문자 알파벳이 가장 먼저 나오고 특수 문자 몇 개가 나온 다음에 소문자 알파벳이 나오네요. Ö
은 거의 마지막에 출력됩니다.
글자는 글자에 대응하는 숫자 형식의 코드를 기준으로 비교됩니다. 코드가 크면 대응하는 글자 역시 크다고 취급되죠.
언어마다 문자 체계가 다르기 때문에 문자열을 "제대로" 비교하는 알고리즘을 만드는 건 생각보다 간단하지 않습니다.
문자열을 비교하려면 일단 페이지에서 어떤 언어를 사용하고 있는지 브라우저가 알아야 합니다.
다행히도 모던 브라우저 대부분이 국제화 관련 표준인 ECMA-402를 지원합니다.
ECMA-402엔 언어가 다를 때 적용할 수 있는 문자열 비교 규칙과 이를 준수하는 메서드가 정의되어있습니다.
str.localeCompare(str2)
를 호출하면 ECMA-402에서 정의한 규칙에 따라 str
이 str2
보다 작은지, 같은지, 큰지를 나타내주는 정수가 반환됩니다.
📌 str
이 str2
보다 작으면 음수를 반환합니다.
📌 str
이 str2
보다 크면 양수를 반환합니다.
📌 str
과 str2
이 같으면 0
을 반환합니다.
alert( "Österreich".localeCompare("Zealand") ); // -1
localeCompare
엔 선택 인수 두 개를 더 전달할 수 있습니다. 기준이 되는 언어를 지정(아무것도 지정하지 않았으면 호스트 환경의 언어가 기준 언어가 됨)해주는 인수와 대·소문자를 구분할지나 "a"
와 "á"
를 다르게 취급할지에 대한 것을 설정해주는 인수가 더 있죠.
자주 사용되는 글자들은 모두 2바이트 코드를 가지고 있습니다. 유럽권 언어에서 사용되는 글자, 숫자, 상형 문자 대다수는 2바이트 표현 체계를 사용합니다.
그런데 2바이트는 65,536()개의 조합밖에 만들어내지 못하기 때문에 현존하는 기호를 모두 표현하기에 충분하지 않습니다. 이를 극복하기 위해 사용 빈도가 낮은 기호는 "서로게이트 쌍(surrogate pair)"이라 불리는 2바이트 글자들의 쌍을 사용해 인코딩합니다.
서로게이트 쌍을 사용해 인코딩한 기호의 길이는 2
입니다.
alert( "𝒳".length ); // 2, 수학에서 쓰이는 대문자 X
alert( "😂".length ); // 2, 웃으면서 눈물 흘리는 얼굴을 나타내는 이모티콘
alert( "𩷶".length ); // 2, 사용 빈도가 낮은 중국어
자바스크립트가 만들어졌을 당시엔 서로게이트 쌍은 존재하지 않았습니다. 따라서 자바스크립트는 서로게이트 쌍으로 표현한 기호를 제대로 처리하지 못합니다.
위 예시에서 기호는 하나지만 길이는 2
인 것을 보고 의아해하실 수 있는데, 이런 이유 때문이죠.
String.fromCodePoint
와 str.codePointAt
은 명세서에 추가된 지 얼마 안 된 메서드로, 서로게이트 쌍을 제대로 처리할 수 있는 몇 안 되는 메서드 입니다. 두 메서드가 등장하기 전에는 String.fromCharCode
와 str.charCodeAt
을 사용했었는데, 이 메서드들은 fromCodePoint
, codePointAt
과 동일하게 동작하지만 서로게이트 쌍은 처리하지 못합니다.
서로게이트 쌍은 두 글자로 취급되기 때문에 기호를 가져오는 게 꽤 까다롭습니다.
alert( "𝒳"[0] ); // 이상한 기호가 출력됨
alert( "𝒳"[1] ); // 서로게이트 쌍의 일부가 출력됨
서로게이트 쌍을 구성하는 글자들은 붙어있을 때만 의미가 있다는 점에 유의해야 합니다. 따라서 위 예시를 실행하면 얼럿창엔 의미 없는 쓰레기 기호가 출력됩니다.
기술적으로 서로게이트 쌍은 서로게이트 쌍에 대응하는 코드를 사용해 감지할 수 있습니다. 글자의 코드가 0xd800..0xdbff
사이에 있으면 이 코드는 서로게이트 쌍을 구성하는 첫 번째 글자를 나타낸다는 것을 알 수 있죠. 이 경우 서로게이트 쌍을 구성하는 두 번째 글자의 코드는 반드시 0xdc00..0xdfff
사이에 있어야 합니다. 범위 0xd800..0xdbff
와 0xdc00..0xdfff
는 표준에서 서로게이트 쌍을 위해 일부러 비워둔 코드입니다.
// charCodeAt는 서로게이트 쌍을 처리하지 못하기 때문에 서로게이트 쌍을 구성하는 부분에 대한 코드를 반환합니다.
alert( "𝒳".charCodeAt(0).toString(16) ); // d835, 0xd800과 0xdbff 사이의 코드
alert( "𝒳".charCodeAt(1).toString(16) ); // dcb3, 0xdc00과 0xdfff 사이의 코드