String
String (문자열)
String 객체는 문자 시퀀스를 표현하고 조작하는 데 사용됩니다. 원문은 MDN의 JavaScript 내장 객체 참고 문서입니다.
본문
String 객체는 문자 시퀀스를 표현하고 조작하는 데 사용됩니다.
문자열은 텍스트 형태로 표현할 수 있는 데이터를 담는 데 유용합니다. 문자열에 가장 많이 쓰이는 연산은 길이를 확인하는 것, +와 += 문자열 연산자로 만들고 연결(concatenate)하는 것, indexOf()로 부분 문자열(substring)의 존재나 위치를 확인하는 것, substring()으로 부분 문자열을 추출하는 것입니다.
문자열 만들기 (Creating strings)
문자열은 기본형(primitive)으로, 문자열 리터럴에서 만들 수도 있고, String() 생성자를 사용해 객체로 만들 수도 있습니다.
const string1 = "A string primitive";
const string2 = 'Also a string primitive';
const string3 = `Yet another string primitive`;
const string4 = new String("A String object");
문자열 기본형과 문자열 객체는 많은 동작을 공유하지만 중요한 차이와 주의점이 있습니다. 문자열 리터럴은 서로 동일하게 취급되는 작은따옴표나 큰따옴표, 또는 백틱(backtick) 문자로 지정할 수 있습니다. 마지막 형태는 템플릿 리터럴(template literal)을 지정하며, 이 형태에서는 표현식을 보간(interpolate)할 수 있습니다.
문자 접근 (Character access)
문자열에서 개별 문자에 접근하는 방법은 두 가지입니다. 첫 번째는 charAt() 메서드입니다: "cat".charAt(1); // "a". 다른 방법은 문자열을 배열-유사 객체로 취급해 개별 문자가 숫자 인덱스에 대응하도록 하는 것입니다: "cat"[1]; // "a". 대괄호 표기법으로 문자에 접근할 때 이러한 속성을 삭제하거나 값을 할당하려는 시도는 성공하지 않습니다. 해당 속성은 쓰기 가능(writable)도 구성 가능(configurable)도 아니기 때문입니다.
문자열 비교 (Comparing strings)
작음/큼 연산자로 문자열을 비교합니다.
const a = "a";
const b = "b";
if (a < b) {
console.log(`${a} is less than ${b}`);
} else if (a > b) {
console.log(`${a} is greater than ${b}`);
} else {
console.log(`${a} and ${b} are equal.`);
}
===와 ==를 포함한 모든 비교 연산자는 문자열을 대소문자 구분(case-sensitive)으로 비교합니다. 대소문자를 무시하고 비교하는 일반적인 방법은 비교 전에 둘 다 같은 대소문자(대문자나 소문자)로 변환하는 것입니다.
function areEqualCaseInsensitive(str1, str2) {
return str1.toUpperCase() === str2.toUpperCase();
}
toUpperCase()로 변환할지 toLowerCase()로 변환할지는 대체로 임의적이며, 라틴 알파벳을 벗어나면 어느 쪽도 완전히 견고하지 않습니다. 예를 들어 독일어 소문자 ß와 ss는 둘 다 toUpperCase()로 SS가 되지만, 터키어 문자 ı는 특별히 toLocaleLowerCase("tr")를 사용하지 않으면 toLowerCase()가 I와 같지 않다고 잘못 보고합니다.
const areEqualInUpperCase = (str1, str2) => str1.toUpperCase() === str2.toUpperCase();
const areEqualInLowerCase = (str1, str2) => str1.toLowerCase() === str2.toLowerCase();
areEqualInUpperCase("ß", "ss"); // true; should be false
areEqualInLowerCase("ı", "I"); // false; should be true
대소문자를 무시한 동등성을 검사하는 로케일 인식의 견고한 해법은 Intl.Collator API나 문자열의 localeCompare() 메서드(둘은 같은 인터페이스 공유)를 sensitivity 옵션을 "accent"나 "base"로 설정해 사용하는 것입니다.
const areEqual = (str1, str2, locale = "en-US") =>
str1.localeCompare(str2, locale, { sensitivity: "accent" }) === 0;
areEqual("ß", "ss", "de"); // false
areEqual("ı", "I", "tr"); // true
localeCompare() 메서드는 strcmp()와 비슷한 방식으로 문자열 비교를 가능하게 합니다 — 로케일 인식 방식으로 문자열을 정렬할 수 있게 합니다.
문자열 기본형과 String 객체 (String primitives and String objects)
JavaScript는 String 객체와 기본형 문자열 값을 구분합니다(불리언과 숫자도 마찬가지). 큰따옴표나 작은따옴표로 표기한 문자열 리터럴, 그리고 생성자가 아닌 컨텍스트(즉 new 키워드 없이 호출한)에서의 String 호출에서 반환된 문자열은 기본형 문자열입니다. 기본형 문자열에서 메서드를 호출하거나 속성 조회가 일어나는 컨텍스트에서 JavaScript는 자동으로 문자열 기본형을 감싸고 그 래퍼 객체에서 메서드를 호출하거나 속성 조회를 수행합니다.
const strPrim = "foo"; // 리터럴은 문자열 기본형
const strPrim2 = String(1); // "1"이라는 문자열 기본형으로 강제 변환
const strPrim3 = String(true);// "true"라는 문자열 기본형으로 강제 변환
const strObj = new String(strPrim); // new String은 문자열 래퍼 객체를 반환
console.log(typeof strPrim); // "string"
console.log(typeof strObj); // "object"
경고: String을 생성자로 사용하는 일은 드물어야 합니다. 문자열 기본형과 String 객체는 eval()을 사용할 때도 다른 결과를 냅니다. eval에 전달된 기본형은 소스 코드로 취급되지만, String 객체는 다른 객체처럼 취급되어 객체를 반환합니다.
const s1 = "2 + 2"; // 문자열 기본형 생성
const s2 = new String("2 + 2"); // String 객체 생성
console.log(eval(s1)); // 4
console.log(eval(s2)); // "2 + 2"
이런 이유로 코드가 기본형 문자열을 기대하는 곳에서 String 객체를 만나면 깨질 수 있습니다. 다만 일반적으로 작성자는 그 구분을 신경 쓸 필요는 없습니다. String 객체는 valueOf() 메서드로 항상 기본형 대응물로 변환할 수 있습니다.
console.log(eval(s2.valueOf())); // 4
문자열 강제 변환 (String coercion)
문자열을 기대하는 많은 내장 연산은 먼저 인자를 문자열로 강제 변환합니다. 그 과정은 다음과 같이 요약됩니다.
- 문자열은 그대로 반환됩니다.
undefined는"undefined"로 변합니다.null은"null"로 변합니다.true는"true"로,false는"false"로 변합니다.- 숫자는
toString(10)과 같은 알고리즘으로 변환됩니다. - BigInt는
toString(10)과 같은 알고리즘으로 변환됩니다. - Symbol은
TypeError를 던집니다. - 객체는 먼저
[Symbol.toPrimitive]()("string" 힌트로),toString(),valueOf()메서드를 이 순서대로 호출해 기본형으로 변환한 뒤, 그 결과 기본형을 문자열로 변환합니다.
JavaScript에서 거의 같은 효과를 얻는 방법은 여러 가지입니다. 템플릿 리터럴 ${x}는 내장된 표현식에 대해 위에서 설명한 문자열 강제 변환 단계를 정확히 수행합니다. String() 함수 String(x)는 같은 알고리즘으로 x를 변환하되, Symbol이 TypeError를 던지지 않고 "Symbol(description)"(description은 Symbol의 설명)을 반환합니다. + 연산자 "" + x는 피연산자를 문자열이 아니라 기본형으로 강제 변환하며, 일부 객체에서는 일반적인 문자열 강제 변환과 완전히 다른 동작을 합니다. 용도에 따라 ${x}(내장 동작을 흉내)나 String(x)(오류 없이 symbol 값을 처리)를 사용하는 것이 좋고, "" + x는 사용하지 말아야 합니다.
UTF-16 문자, 유니코드 코드 포인트, grapheme cluster
문자열은 근본적으로 UTF-16 코드 유닛(code unit)의 시퀀스로 표현됩니다. UTF-16 인코딩에서 각 코드 유닛은 정확히 16비트입니다. 즉 단일 UTF-16 코드 유닛으로 표현 가능한 문자는 최대 2^16 = 65536개입니다. 이 문자 집합을 기본 다국어 평면(BMP, basic multilingual plane)이라고 하며, 라틴·그리스·키릴 알파벳과 많은 동아시아 문자 같은 가장 흔한 문자들을 포함합니다. 각 코드 유닛은 문자열에서 \u 뒤에 정확히 네 자리 16진수를 붙여 쓸 수 있습니다.
그러나 전체 유니코드 문자 집합은 65536보다 훨씬 큽니다. 추가 문자는 UTF-16에서 서로게이트 쌍(surrogate pair)으로 저장되며, 이는 한 문자를 나타내는 16비트 코드 유닛 쌍입니다. 모호성을 피하기 위해 쌍의 두 부분은 0xD8000xDFFF 사이여야 하며, 이 코드 유닛들은 단일 코드 유닛 문자를 인코딩하는 데 사용되지 않습니다. (정확히는 선행 서로게이트(high-surrogate) 코드 유닛은 0xD8000xDBFF(포함), 후행 서로게이트(low-surrogate) 코드 유닛은 0xDC000xDFFF(포함) 값을 갖습니다.) 하나 또는 두 개의 UTF-16 코드 유닛으로 구성된 각 유니코드 문자를 유니코드 코드 포인트라고도 합니다. 각 코드 포인트는 문자열에서 6자리 16진수입니다.\u{xxxxxx}로 쓸 수 있고, 여기서 xxxxxx는 1
**"고아 서로게이트(lone surrogate)"**는 다음 중 하나를 만족하는 16비트 코드 유닛입니다.
- 0xD800~0xDBFF 범위(즉 선행 서로게이트)에 있지만 문자열의 마지막 코드 유닛이거나, 다음 코드 유닛이 후행 서로게이트가 아닌 경우.
- 0xDC00~0xDFFF 범위(즉 후행 서로게이트)에 있지만 문자열의 첫 코드 유닛이거나, 이전 코드 유닛이 선행 서로게이트가 아닌 경우.
고아 서로게이트는 어떤 유니코드 문자도 나타내지 않습니다. 대부분의 JavaScript 내장 메서드는 모두 UTF-16 코드 유닛을 기반으로 동작하므로 이를 올바르게 처리하지만, 고아 서로게이트는 다른 시스템과 상호작용할 때 유효한 값이 아닌 경우가 많습니다 — 예를 들어 encodeURI()는 UTF-8 인코딩을 사용하는데(고아 서로게이트에 대한 인코딩이 없음), 그래서 고아 서로게이트에 대해 URIError를 던집니다. 고아 서로게이트를 포함하지 않는 문자열을 well-formed 문자열이라 하며, encodeURI()나 TextEncoder처럼 UTF-16을 다루지 않는 함수와 안전하게 사용할 수 있습니다. isWellFormed() 메서드로 well-formed 여부를 확인하거나, toWellFormed() 메서드로 고아 서로게이트를 정리할 수 있습니다.
유니코드 문자 위에, 하나의 시각적 단위로 취급되어야 하는 특정 유니코드 문자 시퀀스가 있는데 이를 grapheme cluster라고 합니다. 가장 흔한 경우는 이모지입니다 — 다양한 변형을 가진 많은 이모지가 실제로는 여러 이모지로 구성되며, 보통 <ZWJ>(U+200D) 문자로 결합됩니다.
어느 문자 수준에서 순회하고 있는지 주의해야 합니다. 예를 들어 split("")은 UTF-16 코드 유닛으로 분할하므로 서로게이트 쌍을 분리해 버립니다. 문자열 인덱스도 각 UTF-16 코드 유닛의 인덱스를 가리킵니다. 반면 [Symbol.iterator]()는 유니코드 코드 포인트로 순회합니다. grapheme cluster를 순회하려면 사용자 정의 코드가 필요합니다.
"😄".split(""); // ['\ud83d', '\ude04']; 두 개의 고아 서로게이트로 분할
[...\"👉🏿\"]; // ['👉', '🏿']; 기본 이모지와 "Dark skin tone" 이모지로 분할
[...\"👨👦\"]; // ['👨', '', '👦']; ZWJ로 결합된 "Man"과 "Boy" 이모지로 분할
[...\"🇺🇳\"]; // ['🇺', '🇳']; 두 개의 "지역 표시" 문자 "U"와 "N"으로 분할
생성자 및 정적 메서드 (Constructor and static methods)
String()— String 객체를 만듭니다. 함수로 호출하면 String 타입의 기본형 값을 반환합니다.String.fromCharCode()— 지정된 유니코드 값 시퀀스로 만든 문자열을 반환합니다.String.fromCodePoint()— 지정된 코드 포인트 시퀀스로 만든 문자열을 반환합니다.String.raw()— 원시(raw) 템플릿 문자열로 만든 문자열을 반환합니다.
인스턴스 속성 및 메서드 (Instance members)
String.prototype에 정의되어 모든 인스턴스가 공유하는 constructor, 그리고 각 인스턴스의 고유 속성 length(문자열 길이, 읽기 전용)가 있습니다.
주요 인스턴스 메서드: at()(지정 인덱스의 문자, 음수 지원), charAt(), charCodeAt()(지정 인덱스의 UTF-16 코드 유닛 값), codePointAt(), concat(), endsWith(), includes(), indexOf(), isWellFormed()(고아 서로게이트 포함 여부), lastIndexOf(), localeCompare(), match(), normalize(), padEnd(), padStart(), repeat(), replace()/replaceAll(), search(), slice(), split(), startsWith(), substring()/substr(), toLocaleLowerCase()/toLocaleUpperCase(), toLowerCase()/toUpperCase(), toWellFormed(), toString(), trim()/trimEnd()/trimStart(), valueOf(), [Symbol.iterator]() 등이 있습니다.
예제 (Examples)
기본 사용 예:
"cat".charAt(1); // "a"
const a = "a", b = "b";
a < b; // true
function areEqualCaseInsensitive(str1, str2) {
return str1.toUpperCase() === str2.toUpperCase();
}
사양 및 호환성
String은 ECMAScript 사양에서 정의되며, Baseline "Widely available"로 분류되어 2015년 7월부터 브라우저 전반에서 사용 가능했습니다. 기능의 일부 부분은 지원 수준이 각각 다를 수 있습니다.
더 알아보기
- String.prototype.localeCompare()
- Template literals
- Lexical grammar
- Intl.Collator
- Numbers and strings 가이드