문자열은 어느 언어에나 있으니 새로울 게 없어 보인다. 그런데 JS 문자열엔 초보를 두 번 넘어뜨리는 지점이 있다. 하나는 문자열을 고칠 수 없다는 것, 또 하나는 정규식이 슬래시로 코드 한복판에 박힌다는 것이다. 이 둘을 모르면 "왜 안 바뀌지"와 "이 슬래시 뭉치는 뭐지" 사이에서 시간을 버린다.


이번 편은 매일 쓰는 문자열 메서드와 정규식 기본을 상황과 실행 결과로 짚는다. 정규식은 외계어처럼 보여도 몇 개 기호와 test, match, replace만 잡으면 갑자기 손에 들어온다.


문자열은 못 고친다. JS 문자열은 불변(immutable)이다. 인덱스로 한 글자를 바꿔치기하려 해도 조용히 무시된다. 모든 변형 메서드는 원본을 놔두고 바뀐 새 문자열을 돌려줄 뿐이다.


let s = "hello";
s[0] = "H";
console.log(s);
console.log(s.toUpperCase());
console.log(s);


s[0] = "H"를 해도 s는 여전히 hello다. 에러도 안 나고 그냥 안 먹힌다. toUpperCase()HELLO를 주지만 이건 새 문자열이고, 그 아래 s는 또 hello 그대로다. 그래서 바꾼 결과를 쓰려면 s = s.toUpperCase()처럼 반드시 되받아야 한다. 이걸 몰라 "메서드를 불렀는데 왜 안 바뀌냐"고 헤매는 게 첫 관문이다.


템플릿 리터럴로 값을 끼워 넣는다. 따옴표 대신 백틱으로 감싸면 문자열 안에 ${ }로 값을 바로 박을 수 있다. 더하기로 이어 붙이던 지저분한 코드가 사라진다.


const name = "hana", n = 3;
console.log(`${name}님, 남은 횟수 ${n}회`);


결과는 hana님, 남은 횟수 3회다. ${name} 자리에 변수 값이 들어가고, 중괄호 안엔 변수뿐 아니라 ${n - 1} 같은 식도 넣을 수 있다. 백틱은 줄바꿈도 그대로 품어서 여러 줄 문자열을 만들 때도 편하다.


자르고 찾는 메서드. 위치로 잘라내는 slice, 뒤에서 세는 at, 위치를 주는 indexOf가 기본기다. slice는 배열에서 봤듯 음수 인덱스로 뒤에서 자를 수 있다.


const str = "javascript";
console.log(str.length);
console.log(str.at(-1));
console.log(str.slice(0, 4));
console.log(str.slice(-6));
console.log(str.indexOf("script"));


length10, 마지막 글자 at(-1)t다. slice(0, 4)는 0번부터 4번 앞까지라 java, 끝 인덱스는 포함하지 않는다는 게 함정이다. slice(-6)은 뒤에서 여섯 글자인 script, indexOf("script")는 그게 시작하는 자리 4를 준다. 없는 걸 indexOf로 찾으면 0이 아니라 -1이 나오니, 존재 여부는 includes로 보는 게 헷갈리지 않는다.


쪼개고 합친다. split은 문자열을 특정 구분자로 잘라 배열로 만들고, 배열의 join은 그 반대로 다시 문자열로 붙인다. 콤마로 이어진 데이터를 다룰 때 늘 짝으로 쓴다.


const csv = "kim,lee,park";
const arr = csv.split(",");
console.log(arr);
console.log(arr.join(" / "));
console.log("a-b-c".split(""));


split(",")[ 'kim', 'lee', 'park' ], join(" / ")kim / lee / park를 준다. 구분자를 빈 문자열로 주면 [ 'a', '-', 'b', '-', 'c' ]처럼 글자 하나하나로 쪼개진다. 문자열을 배열로 바꿔 map이나 filter를 태우고 싶을 때 이 빈 문자열 split이 다리 역할을 한다.


바꾸기는 replace와 replaceAll이 다르다. 문자열로 replace를 부르면 딱 첫 번째 하나만 바꾼다. 전부 바꾸려면 replaceAll을 쓰거나, 뒤에 나올 정규식에 g 플래그를 붙여야 한다.


console.log("a-b-a".replace("a", "X"));
console.log("a-b-a".replaceAll("a", "X"));


replaceX-b-a로 앞의 a만 바꾸고, replaceAllX-b-X로 둘 다 바꾼다. 나는 예전에 replace로 모든 걸 바꾼 줄 알고 넘어갔다가, 두 번째 값이 안 바뀐 채 흘러가 한참 뒤에야 원인을 찾았다. 여러 개면 replaceAll이라고 외워두면 편하다.


정규식은 패턴으로 검사한다. 슬래시 두 개 사이에 규칙을 적은 게 정규식이다. \d는 숫자 한 자리, {3}은 세 번 반복이라는 뜻이다. test는 그 패턴이 문자열에 있는지를 참거짓으로 준다.


const re = /\d{3}-\d{4}/;
console.log(re.test("012-3456"));
console.log(re.test("hello"));


012-3456은 숫자 셋, 하이픈, 숫자 넷의 모양에 맞으니 true, hello는 안 맞으니 false다. 형식 검사, 그러니까 전화번호나 이메일이 규칙에 맞는지 볼 때 test가 가장 손이 덜 간다. 값을 꺼낼 필요 없이 맞는지만 보면 될 때 쓴다.


match로 꺼내고 g로 전부 찾는다. 실제로 걸린 값을 꺼내려면 match다. 그냥 쓰면 첫 매치 하나와 그 부가 정보를 담은 배열을, g 플래그를 붙이면 걸린 값 전부를 배열로 준다.


console.log("주문 12개, 취소 3개".match(/\d+/));
console.log("주문 12개, 취소 3개".match(/\d+/g));


\d+는 숫자 한 자리 이상이라는 뜻이다. g 없이 부르면 첫 숫자만 잡아 [ '12', index: 3, ... ] 꼴로, 값 말고도 몇 번째에서 걸렸는지까지 딸려 온다. g를 붙이면 부가 정보 없이 걸린 값만 모아 [ '12', '3' ]을 준다. 개수만큼 다 뽑고 싶으면 g를 잊지 말아야 한다.


괄호로 부분을 캡처한다. 정규식 안에 소괄호를 치면 그 부분이 따로 뽑혀 나온다. 날짜에서 연, 월, 일을 각각 떼어내 보자.


const m = "2026-07-11".match(/(\d{4})-(\d{2})-(\d{2})/);
console.log(m[1], m[2], m[3]);


결과는 2026 07 11이다. m[0]엔 전체 매치가 들어가고 괄호로 묶은 것들이 m[1]부터 차례로 담긴다. 괄호가 많아지면 번호를 세기 번거로운데, 이럴 땐 (?<year>\d{4})처럼 이름을 붙여 m.groups.year로 꺼내면 번호 대신 이름으로 읽혀 훨씬 덜 헷갈린다.


replace에 정규식을 넣으면 패턴으로 바꾼다. 바꿀 대상을 글자가 아니라 패턴으로 지정할 수 있다. 게다가 캡처한 부분을 $1, $2로 자리 바꿔 끼울 수도 있다.


console.log("010-1234-5678".replace(/\d/g, "*"));
console.log("hana kim".replace(/(\w+)\s(\w+)/, "$2 $1"));


앞은 모든 숫자를 별표로 바꿔 ***-****-****가 되고, 뒤는 두 단어를 잡아 순서를 뒤집어 kim hana가 된다. $1은 첫 괄호, $2는 둘째 괄호에 걸린 값이다. 이름과 성 순서를 바꾸거나 날짜 형식을 갈아끼우는 일이 이 자리 바꿈 하나로 끝난다.


문자열을 다룰 때 나는 두 가지를 잊지 않으려 한다. 메서드는 원본을 안 고치니 결과를 반드시 되받을 것, 정규식은 겁먹지 말고 작은 문자열에 testmatch로 직접 찍어볼 것. \d, \w, +, { }, g 이 몇 개만 손에 익어도 형식 검사와 값 추출, 치환의 팔 할이 해결된다. 복잡한 패턴은 정규식 테스트 사이트에 붙여 한 조각씩 맞춰가면 결국 읽힌다.