문자열의 기본 특성
JavaScript에서 문자열은 원시 타입 중 하나이며, 변경 불가능(immutable) 합니다. 즉, 한 번 생성된 문자열을 직접 수정할 수 없고, 수정된 '새로운 문자열'을 만들어서 반환합니다.
String 객체와 원시 문자열
원시 문자열(primitive string)
let str = "Hello";
이는 JS 엔진 내부적으로 문자열 리터럴을 저장하며, 상수 풀(String pool)에 캐싱될 수도 있습니다.
String 객체
let objStr = new String("Hello");
이는 래퍼(wrapper) 객체입니다. 원시 문자열에 대해 객체 형태의 프로퍼티와 메서드를 사용할 때 내부적으로 임시 `String` 객체로 감싸주지만, 가급적 직접 `new String(...)` 방식보다는 원시 문자열 리터럴을 사용하는 것이 권장됩니다.
- 타입 혼동(Type Confusion)
- `String` 객체는 `typeof` 연산자로 검사할 때 object로 반환됩니다.
- 반면 원시 문자열은 `typeof`로 string을 반환합니다.
- 문제점 : `==` 비교는 같다고 판단할 수 있지만, `===`는 타입이 다르기 때문에 `false`가 됩니다. 이는 의도치 않은 버그를 유발할 가능성이 있습니다.
- 불필요한 메모리 및 성능 문제
- `String` 객체를 생성하면, 내부적으로 더 많은 메모리를 소비하며, 실제로는 간단한 텍스트 조작을 위해 불필요한 오버헤드가 발생합니다.
- 원시 문자열은 필요한 경우, 자동으로 임시 `String` 객체로 래핑되기 때문에 굳이 명시적으로 `new String()`을 호출할 필요가 없습니다.
- API 호환성 및 예상치 못한 동작
- `new String("")`은 객체이기 때문에 비어 있어도 truthy로 평가됩니다.
- 반면 `""`(빈 문자열)은 falsy입니다.
- 따라서 `if (str)` 같은 조건문에서 `String` 객체는 항상 true로 평가됩니다.
function checkString(str) {
if (str) {
console.log("String is not empty");
}
}
checkString(new String("")); // "String is not empty" (비어 있음에도 불구하고 실행됨)
checkString(""); // 아무것도 출력되지 않음 (정상 동작)
문자열과 배열의 유사점과 차이점
문자열도 인덱스로 각 문자를 접근할 수 있어 배열처럼 보이지만(`str[0]`), 실제로는 불변이고. `length` 이외에 배열에서 제공하는 여러 메서드(`push`, `pop`, `splice` 등)는 사용할 수 없습니다.
`for...of` 문을 이용해 문자열의 각 문자를 순회할 수 있습니다.
let str = "ABC";
for (let ch of str) {
console.log(ch);
}
// A
// B
// C
문자열 생성과 템플릿 리터럴
문자열 리터럴(Literal)
- 작은따옴표 `' '`
- 큰따옴표 `" "`
- 백틱(ES6 템플릿 리터럴) `
let single = 'Hello';
let double = "Hello";
let backtick = `Hello`;
템플릿 리터럴(Template Literal)
ES6에서 도입된 문법으로, 표현식 삽입과 여러 줄 문자열을 간편하게 작성할 수 있습니다.
let name = "John";
let greeting = `Hello, ${name}!
Welcome to JavaScript.`;
또한 태그드 템플릿(Tagged Template) 기능을 이용하면, 템플릿 리터럴 내부의 문자열과 표현식을 분리해 특별한 처리를 할 수도 있습니다.
function highlight(strings, ...values) {
// strings: ['My name is ', ' and I am ', ' years old.']
// values: ['John', 20]
return strings.reduce((acc, str, i) => {
return acc + str + (values[i] ? `<strong>${values[i]}</strong>` : "");
}, "");
}
let name2 = "John";
let age = 20;
let result = highlight`My name is ${name2} and I am ${age} years old.`;
console.log(result);
// My name is <strong>John</strong> and I am <strong>20</strong> years old.
문자열 메서드 총정리
검색 / 판별
메서드 | 설명 |
`indexOf(substr)` | 문자열 `substr`를 찾고, 첫 번째 인덱스를 반환 (없으면 -1) |
`lastIndexOf(substr)` | 문자열 `substr`를 뒤에서부터 찾고, 인덱스를 반환 (없으면 -1) |
`includes(substr)` | 문자열에 `substr`가 포함되어 있는지 여부 (true/false) |
`startsWith(substr)` | 문자열이 `substr`로 시작하는지 여부 (true/false) |
`endsWith(substr)` | 문자열이 `substr`로 끝나는지 여부 (true/false) |
`search(regexp)` | 정규 표현식 `regexp`에 매칭되는 첫 번째 인덱스 반환 (없으면 -1) |
`match(regexp)` | 정규 표현식 `regexp`와 매칭되는 결과(배열 혹은 null) 반환 |
`matchAll(regexp)` | 정규 표현식 `regexp`에 매칭되는 모든 결과(이터레이터) 반환 (ES2020) |
matchAll 예시
let str = "test1test2";
let regexp = /t(e)(s)(t)/g;
let matches = str.matchAll(regexp);
for (let match of matches) {
console.log(match);
// 예: ["test", "e", "s", "t", index: 0, input: "test1test2", groups: undefined]
// ["test", "e", "s", "t", index: 5, input: "test1test2", groups: undefined]
}
추출
메서드 | 설명 |
`charAt(index)` | 해당 인덱스 위치의 문자 반환 (인덱스 벗어나면 빈 문자열 반환) |
`charCodeAt(index)` | 해당 인덱스 위치 문자의 UTF-16 코드 유닛 값(0–65535 사이 정수) 반환 |
`codePointAt(index)` | UTF-16이 아닌 유니코드 코드 포인트(서로게이트 문자 포함)에 안전하게 접근 가능 (ES6) |
`slice(start, end)` | 문자열의 일부를 추출해 새 문자열 반환 (음수 인덱스 가능) |
`substring(start, end)` | `start`와 `end` 사이의 문자열을 반환 (음수는 0으로 처리) |
`substr(start, length)` | `start`부터 `length` 길이만큼 반환 (현재는 비추천 Deprecated) |
let str = "JavaScript 💖";
console.log(str.charAt(4)); // S
console.log(str.slice(0, 4)); // Java
console.log(str.substring(4, 10)); // Script
console.log(str.codePointAt(11)); // 💖의 유니코드 코드 포인트
`substr()`는 과거부터 사용되어 왔지만, 현대는 Deprecated 상태이므로 가능한 `slice()`나 `substring()`을 사용하시는 것이 좋습니다.
변환
메서드 | 설명 |
`toUpperCase()` | 모두 대문자로 변환 |
`toLowerCase()` | 모두 소문자로 변환 |
`toLocaleUpperCase()` | 지역화(로케일)에 맞춰 대문자로 변환 (예: 터키어 i → İ) |
`toLocaleLowerCase()` | 지역화(로케일)에 맞춰 소문자로 변환 |
`normalize(form)` | 유니코드 정규화 (NFC, NFD, NFKC, NFKD 등) |
// 예: 한글 “가”는 유니코드 U+AC00, 또는 자모를 결합한 형태로도 표현 가능
let str1 = "가";
let str2 = "\u1100\u1161"; // ㄱ + ㅏ 조합
console.log(str1 === str2); // false, 다른 코드 포인트 시퀀스
console.log(str1.normalize() === str2.normalize()); // true
조작
메서드 | 설명 |
`replace(찾을값, 바꿀값)` | 첫 번째로 일치하는 패턴만 바꿔서 새 문자열 반환. 정규 표현식 가능 |
`replaceAll(찾을값, 바꿀값)` | 일치하는 모든 패턴을 바꿔서 새 문자열 반환 (ES2021) |
`trim()` | 양쪽 공백(개행 문자 포함) 제거 |
`trimStart()` / `trimEnd()` | 문자열 앞/뒤 공백만 제거 (이전 이름은 `trimLeft()`, `trimRight()`) |
`padStart(targetLength, padStr)` | 문자열 길이가 `targetLength`가 될 때까지 앞쪽에 `padStr`를 반복해서 붙인 새 문자열 반환 |
`padEnd(targetLength, padStr)` | 문자열 길이가 `targetLength`가 될 때까지 뒤쪽에 `padStr`를 반복해서 붙인 새 문자열 반환 |
let str = " Hello JavaScript! ";
console.log(str.replace("Java", "Type")); // " Hello TypeScript! "
console.log(str.replaceAll(" ", "*")); // "**Hello*JavaScript!**"
console.log(str.trim()); // "Hello JavaScript!"
console.log(str.padStart(25, "*")); // "***** Hello JavaScript!"
분리 / 결합 / 반복
메서드 | 설명 |
`split(separator)` | `separator`로 분리하여 배열 반환 |
`concat(...strs)` | 여러 문자열을 결합해 새 문자열 반환 |
`repeat(count)` | 문자열을 `count`번 반복해 이어붙인 새 문자열 반환 |
`localeCompare()` | 현재 문자열과 인자로 받은 문자열을 비교하여 -1, 0, 1로 결과 반환 (정렬 시 유용) |
let fruit = "apple,orange,banana";
console.log(fruit.split(",")); // ['apple', 'orange', 'banana']
console.log("Hello".concat(" World!")); // "Hello World!"
console.log("hi ".repeat(3)); // "hi hi hi "
console.log("b".localeCompare("a")); // 1 (사전순으로 b > a)
고급 주제
서로게이트 쌍(Surrogate Pair)와 이모지(Emoji) 다루기
JS 문자열은 내부적으로 UTF-16 코드 유닛(0 ~ 65536) 기반으로 관리합니다. 유니코드가 65535(0xFFFF) 범위를 넘는 문자(예: 이모지, 일부 한자)는 서로게이트 쌍(Surrogate Pair)으로 표현됩니다. 따라서 길이가 2인 문자도 있고, `charAt`과 `charCodeAt`만으로는 제대로 처리하기 어려울 수 있습니다. ES6 이후 제공되는 `codePointAt`, `fromCodePoint` 메서드를 사용하면 비교적 안전하게 이모지 등을 다룰 수 있습니다.
let smile = "😊";
console.log(smile.length); // 2, 서로게이트 쌍
console.log(smile.charAt(0)); // � (깨진 문자)
console.log(smile.codePointAt(0)); // 128522 (UTF-32 코드 포인트)
// 문자열 생성
let heart = String.fromCodePoint(0x1F49C);
console.log(heart); // 💜
정규 표현식(Regular Expression)과 함께 쓰기
문자열 검색, 치환, 추출 등을 정규 표현식과 함께 사용하면 더 강력합니다. `match`, `matchAll`, `search`, `replace`, `replaceAll`, `split` 등에서 정규 표현식을 사용할 수 있습니다.
let text = "123-456-7890";
let result = text.replace(/\D/g, ""); // 숫자 이외(\D)를 모두 제거
console.log(result); // "1234567890"
국제화(i18n)와 지역화(l10n)
문자열을 비교하거나 대소문자로 변환할 때, 언어와 문화권(로케일, locale)에 따라 결과가 달라질 수 있습니다. `toLocaleUpperCase(locale)`, `toLocaleLowerCase(locale)`, `localeCompare(locale, options)` 등을 적절히 활용하면 정확한 정렬, 비교, 변환이 가능합니다.
let turkishI = "ı"; // 터키 소문자
console.log(turkishI.toUpperCase()); // I (영문)
console.log(turkishI.toLocaleUpperCase("tr-TR"));// İ (터키 문자)
성능과 메모리
문자열은 불변이므로, 많은 수의 문자열을 조합하거나 잘라낼 때, 잦은 새 문자열 생성은 비용이 커질 수 있습니다. 대량의 문자열을 다룰 때는 배열에 담아 두었다가 최종적으로 `join`으로 합치는 방식을 자주 사용합니다. (예: 로그 누적, 긴 HTML 문자열 작성)
let builder = [];
for (let i = 0; i < 1000; i++) {
builder.push(`Line ${i}`);
}
let finalStr = builder.join("\n");
마무리
정리하자면, JavaScript의 `String`(원시 문자열)은 다루기 쉽고 강력한 기능을 제공하지만, 불변 구조, 유니코드 이슈, 정규 표현식 활용 같은 세부 사항을 잘 이해해야 버그 없이 안정적으로 사용할 수 있습니다. 위 내용을 토대로 다양한 문자열 처리 상황에 맞춰 적절히 활용해 보세요.
'JAVASCRIPT' 카테고리의 다른 글
[React] Virtual DOM: 리액트의 핵심 기술 (0) | 2025.01.09 |
---|---|
[React] 리액트적으로 사고하기(번역) (1) | 2025.01.01 |
자바스크립트와 브라우저의 비동기 처리 구조: 이벤트 루프와 큐 (1) | 2024.12.28 |
배열 생성 심화: Array / Array.of / Array.from (0) | 2024.12.24 |
자바스크립트 배열(Array)의 모든 것 (0) | 2024.12.21 |