JS/[web] 코어 JS 튜토리얼

[코어 JS] 5.3 - 문자열

web_seul 2021. 10. 23. 14:42
반응형

5.3 문자열

JS에는 글자 하나만 저장할 수 있는 별도의 자료형이 없음, 텍스트 형식의 데이터 길이에 상관없이 문자열 형태로 저장

JS에서 문자열을 인코딩 방식과 무관하게 항상 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)'에서도 사용, func`string`으로 사용하면 백틱 안의 문자열 조각이나 표현식 평가 결과를 인수로 받아 자동으로 호출됨(tagged template), 이를 사용하면 사용자 지정 템플릿에 맞는 문자열 만들기에 용이 

특수 기호

'특수기호 \n '줄바꿈 문자(newline character)'를 사용하면 작은따옴표나 큰따옴표로 여러줄의 문자열 가능

let guestList = "손님:\n * John\n * Pete\n * Mary";

alert(guestList); // 손님 리스트를 여러 줄에 걸쳐 작성함
let str1 = "Hello\nWorld"; // '줄 바꿈 기호'를 사용해 두 줄짜리 문자열을 만듦

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

alert(str1 == str2); // true

 

특수 문자 설명
\n 줄 바꿈
\r 캐리지 리턴(carriage return), Windows에선 캐리지 리턴과 줄 바꿈 특수 문자를 조합(\r\n)으로 줄바꿈, 단독 사용x
\', \" 따옴표
\\ 역슬래시
\t
\b, \f, \v 각 백스페이스(backspace), 폼피드(form feed), 세로탭(vertical tab)으로 사용하지않지만 호환성 유지를 위해 남아있는 기호
\xXX 16진수 유니코드 XX로 표현한 유니코드 글자(ex. 알파벳'z'는 '\x7A'와 동일)
\uXXXX UTF-16 인코딩 규칙을 사용하는 16진수 코드 XXXX로 표현한 유니코드 기호, XXXX는 반드시 네 개의 16진수로 구성되어야 함(ex. \u00A9는 저작권 기호 ⓒ의 유니코드(
\u(X..X) 1~6개 사이의 16진수 UTF-32로 표현한 유니코드 기호, 몇몇의 특수한 글자는 두개의 유니코드 기호를 사용해 인코딩되므로 4바이트를 차지함, 이 방법으로 긴 코드 삽입 가능
alert( "\u00A9" ); // ©
alert( "\u{20331}" ); // 佫, 중국어(긴 유니코드)
alert( "\u{1F60D}" ); // 😍, 웃는 얼굴 기호(긴 유니코드)

모든 특수 문자는 이스케이프 문자(escape character)라고도 불리는 역슬래시(backslash character) \로 시작

문자열 내 따옴표 넣을때 사용 가능

alert( 'I\'m the Walrus!' ); // I'm the Walrus!

백틱으로 문자열 감싸서도 사용 가능

alert( `I'm the Walrus!` ); // I'm the Walrus!

역슬래시 \는 문자열을 정확히 읽기위한 용도로 역할이 끝나면 사라져서 메모리에 저장되는 문자열에는 \가 없음

\를 보여줘야 하는 경우에는 \\ 로 사용

alert( `역슬래시: \\` ); // 역슬래시: \

 

문자열의 길이

length 프로퍼티엔 문자열의 길이 저장

alert( `My\n`.length ); // 3

\n은 '특수문자'하나로 추급되므로 My\ㅜ의 길이는 3

 

! length는 프로퍼티

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

문자열 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'; // as를 찾아봅시다.

let pos = 0;
while (true) {
  let foundPos = str.indexOf(target, pos);
  if (foundPos == -1) break;

  alert( `위치: ${foundPos}` );
  pos = foundPos + 1; // 다음 위치를 기준으로 검색을 이어갑니다.
}

동일한 알고리즘으로 짧게 줄이기

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)

indexOf와 유사한 기능으로 문자열 끝에서 부터 문자열을 찾음, 반환되는 부분도 문자열 끝이 기준

 

if문에서 indexOf를 사용할 때 주의점

let str = "Widget with id";

if (str.indexOf("Widget")) {
    alert("찾았다!"); // 의도한 대로 동작하지 않습니다.
}
//str.indexOf("Widget") 은 0을 반환하는데
//if문에서는 0을 false로 간주하므로
//alert창이 뜨지 않음
let str = "Widget with id";

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

 

비트 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("...")를 사용하여 코드길이 줄이기(권장x, 오래된 타입)

let str = "Widget";

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

if(~str.indexOf(...))는 '부분 문자열인지 확인'하는 코드

참고로 -1이외에도 ~연산자 적용시 0을 반환하는 숫자는 다양, 아주 큰 숫자에 ~ 연산자를 적용하면 32비트 정수로 바꾸는 과정에서 잘림 현상 발생, 그 중 가장 큰 숫자는 4294967295 (~4294967295는 0), 문자열이 아주 길지 않은 경우에만 ~연산자가 의도한 대로 작동함

모던 JS에서는 .includes 메서드를 사용해 부분 문자열 포함 여부 검사

includes, startsWith, endsWith

str.includes(substr, pos)는 str에 부분 문자열 substr이 있는지에 따라 true, false 반환, 부분 문자열의 위치 정보는 필요하지않고 포함 여부만 알고싶을 때 적합

alert( "Widget with id".includes("Widget") ); // true
alert( "Hello".includes("Bye") ); // false

두번째 인수를 넘기면 해당 위치부터 문자열 검색

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

str.startWith, str.endsWith는 문자열 str이 특정 문자로 시작하는지(start with), 끝나는지(end with)여부 확인시 사용

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

 

부분 문자열 추출하기

str.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번째 위치의 자는 포함하지 않음)

두번째 인수가 생략된 경우 명시한 위치부터 문자열 끝까지 반환

let str = "stringify";
alert( str.slice(2) ); // ringify, 2번째부터 끝까지

start, end는 음수가 될 수 있음, 음수를 넘기면 문자열 끝에서 카운팅 시작

let str = "stringify";

// 끝에서 4번째부터 시작해 끝에서 1번째 위치까지
alert( str.slice(-4, -1) ); // gif

 

str.substring(start [, end]) : start와 end사이 문자열 반환, 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으로 처리됨

 

str.substr(start [, length]) : start부터 length개의 글자 반환

let str = "stringify";
alert( str.substr(2, 4) ); // ring, 두 번째부터 글자 네 개

첫번째 인수가 음수면 뒤에서 부터

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

 

부분 문자열 추출 메서드

메서드 추출할 부분 문자열 음수 허용 여부(인수)
slice(start, end) start부터 end까지 (end 미포함) 음수허용
substring(start, end) start와 end사이 음수는 0으로 취급
substr(start, length) start부터 length개의 글자 음수 허용

 

! 어떤 메서드를 선택해야 하나요

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

slice는 음수 인수를 허용해서 substring보다 좀 더 유연

 

문자열 비교하기

문자열은 알파벳 순서를 기준으로 글자끼리 비교

1. 소문자는 대문자보다 항상 큼

alert( 'a' > 'Z' ); // true

2. 발음 구별기호(diacritical mark)가 붙은 문자는 알파벳 순서 기준을 따르지 않음

alert( 'Österreich' > 'Zealand' ); // true (Österreich는 오스트리아를 독일어로 표기한 것임 - 옮긴이)

 

JS내부에서 문자열이 표시되는 방식

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를 지원하는 브라우저엥서 str.localeCompare(str2)를 호출하면 str이 str2보다 작은지 큰지, 같은지를 나타내는 정수가 반환됨

- str < str2 : 음수

- str > str2 : 양수

- str = str2 : 0

alert( 'Österreich'.localeCompare('Zealand') ); // -1

localCompare 엔 선택 인수 두개를 더 전달할 수 있음, 기준이 되는 언어를 지정해주는 인수와 대소문자 구분여부, a와 a'를 다르게 취급할지에 대한 여부를 설정해주는 인수

 

문자열 심화

- 서로게이트 쌍

글자들은 대부분 2바이트 코드를 가지는데 65,536개의 조합만 생성되므로 기호를 모두 표현하기에는 부족함, 이를 극복하기 위해 사용 빈도가 낮은 기호는 '서로게이트 쌍(surrogate pair)'이라는 2바이트 글자들의 쌍을 사용해 인코딩, 서로게이트 쌍을 사용해 인코딩한 기호의 길이는 2

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

JS는 서로게이트 쌍을 표현한 기호를 제대로 처리하지 못함

String.fromCodePoint 와 str.codePointAt는 서로게이트 쌍을 제대로 처리하는 몇 안되는 메서드, 이전에 사용한 String.fromCharCode와 str.charCodeAt는 동일하게 동작하나 서로게이트 쌍은 처리하지못함

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

서로게이트 쌍을 구성하는 글자들은 붙어있을 때만 의미가 있음, 따라서 위의 예시는 의미없는 기호가 출력됨

기술적으로 서로게이트 쌍은 대응하는 코드를 사용해 감지, 글자의 코드가 0xd800..0xdbff사이에 있으면 서로게이트 쌍을 구성하는 글자임을 인식하도록 사이값은 비워져있음

// charCodeAt는 서로게이트 쌍을 처리하지 못하기 때문에 서로게이트 쌍을 구성하는 부분에 대한 코드를 반환합니다.

alert( '𝒳'.charCodeAt(0).toString(16) ); // d835, 0xd800과 0xdbff 사이의 코드
alert( '𝒳'.charCodeAt(1).toString(16) ); // dcb3, 0xdc00과 0xdfff 사이의 코드

 

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

조합 가능한 글자의 수가 너무 많기 떄문에 UTF-16에서는 베이스 글자 뒤에 유니코드 문자를 붙여 베이스 글자를 꾸밀 수 있도록 유니코드 글자를 남겨둠

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

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

 

요약

- JS에는 세 종류의 따옴표가 있는데 이 중 하나인 백틱(`)은 문자열을 여러줄에 걸쳐 쓸 수 있게 하고 문자열 중간에 ${..}를 사용해 표현식도 넣을 수 있다는 특징이 있다

- JS에서는 UTF-16을 사용해 문자열을 인코딩한다

- \n 같은 특수 문자를 사용할 수 있다, \n..을 사용하면 해당 문자의 유니코드를 사용해 글자를 만들 수 있다

- 문자열 내의 글자 하나를 얻으려면 대괄호 [] 사용

- 부분 문자열을 얻으려면 slice나 substring 사용

- 소문자로 바꾸려면 toLowerCase, 대문자로 바꾸려면 toUpperCase 사용

- indexOf를 사용하면 부분 문자열의 위치를 얻을 수 있음, 존재 여부만 확인하려면 includes/startsWith/endsWith 사용

- 특정 언어에 적합한 비교 기준을 사용해 문자열을 비교할 때는 localeCompare를 사용, 이 메서드가 없으면 글자 코드를 기준으로 문자열이 비교됨

 이 외의 문자열에 쓸 수 있는 유용한 메서드

- str.trim() : 문자열 앞과 끝의 공백 문자를 다듬어 줌(제거함)

- str.repeat(n) : 문자열을 n번 반복함

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

반응형