문자열에서 특정 패턴을 찾아 바꾸는 작업은 개발에서 정말 자주 마주친다. 주소록 데이터를 정제할 때, API 응답값의 특수문자를 처리할 때, URL 쿼리 파라미터를 인코딩할 때도 그렇다. 대부분의 개발자들은 그냥 replace()를 쓰거나, 정규표현식과 g 플래그를 붙여서 구글에서 본 코드를 복사한다. 다만 replace()와 replaceAll()의 차이, 언제 어떤 메서드를 써야 하는지, 성능상 차이가 있는지 정확히 이해하는 개발자는 의외로 드물다.
이번 글에서는 replace() 메서드의 동작 원리부터 시작해, replaceAll()이 왜 필요했는지, 정규표현식과의 조합은 어떻게 하는지, 그리고 실무에서 자주 하는 실수까지 완벽하게 정리해서 소개하겠다.
가장 중요한 포인트부터 시작하자. replace()는 문자열에서 처음 발견한 패턴 딱 하나만 바꾼다. 나머지는 그대로 둔다.
const text = "apple apple apple";
const result = text.replace("apple", "orange");
console.log(result);
// 출력: "orange apple apple"
보다시피 첫 번째 apple만 orange로 바뀌고, 나머지 두 개의 apple은 그대로다. 만약 모든 apple을 orange로 바꾸려면 어떻게 해야 할까? 예전에는 정규표현식에 g(global) 플래그를 붙여야 했다.
const text = "apple apple apple";
const result = text.replace(/apple/g, "orange");
console.log(result);
// 출력: "orange orange orange"
정규표현식 /apple/g는 문자열 전체에서 apple을 찾는다. 모든 매칭되는 부분을 orange로 바꾼다. 이 방식은 예전부터 널리 써왔고 지금도 많은 개발자들이 쓰고 있다.
replaceAll()은 ES2021(ES12)에 추가된 비교적 새로운 메서드다. 이름 그대로 모든 매칭되는 부분을 한 번에 바꾼다. 정규표현식이 필요 없다.
const text = "apple apple apple";
const result = text.replaceAll("apple", "orange");
console.log(result);
// 출력: "orange orange orange"
코드가 훨씬 간결하고 읽기도 쉽다. 단순 문자열을 치환할 때는 정규표현식을 기억할 필요가 없다. replaceAll()은 평문 문자열과 정규표현식 둘 다 지원한다.
const text = "apple apple apple";
// 평문 문자열 사용
const result1 = text.replaceAll("apple", "orange");
// 정규표현식 사용 (g 플래그 필수)
const result2 = text.replaceAll(/apple/g, "orange");
console.log(result1);
// 출력: "orange orange orange"
console.log(result2);
// 출력: "orange orange orange"
다만 replaceAll()에서 정규표현식을 사용할 때는 반드시 g 플래그를 붙여야 한다. g 플래그 없이 사용하면 TypeError가 발생한다.
| 메서드 | 첫 번째만 바꿈 | 모두 바꿈 | 정규표현식 지원 | 평문 문자열 지원 | 브라우저 호환성 |
|---|---|---|---|---|---|
| replace() | ○ | ○ (g 플래그 필요) | ○ | ○ | 모든 브라우저 |
| replaceAll() | × | ○ | ○ (g 플래그 필수) | ○ | IE 11 미지원 |
이제 실제로 개발하면서 만날 수 있는 예제들을 보자.
API 요청 시 파라미터에 특수문자가 포함되면 공백을 + 기호로 바꿔야 할 때가 있다. 모든 공백을 바꿔야 하므로 replaceAll()이 적합하다.
// ✗ 잘못된 것: 첫 번째 공백만 바뀜
const query = "hello world nice day";
const wrong = query.replace(" ", "+");
console.log(wrong);
// 출력: "hello+world nice day" (첫 번째 공백만 +로 바뀜)
// ✓ 올바른 것: 모든 공백이 바뀜
const right = query.replaceAll(" ", "+");
console.log(right);
// 출력: "hello+world+nice+day"
API에서 받은 데이터에 큰따옴표 안의 쉼표는 유지하되, 외부 쉼표만 보존해야 할 때 정규표현식으로 복잡하게 구성해야 한다. 하지만 단순히 특정 문자를 모두 제거하면 replaceAll()이 간편하다.
// CSV 행의 앞뒤 공백 제거
const csvRow = " item1 , item2 , item3 ";
const cleaned = csvRow.replaceAll(" ", "");
console.log(cleaned);
// 출력: "item1,item2,item3"
API 응답에 포함된 HTML 특수문자를 안전하게 출력하려면 엔티티로 변환해야 한다.
function escapeHTML(text) {
return text
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
const userInput = '';
const safe = escapeHTML(userInput);
console.log(safe);
// 출력: <script>alert("XSS")</script>
체이닝으로 여러 문자를 연속으로 치환할 수 있다. 각 replaceAll()이 이전 결과에 작용하므로 순서가 중요할 수 있다. 위 예제에서 &를 먼저 치환하지 않으면 < 같은 엔티티가 다시 치환될 위험이 있다.
숫자만 남기고 나머지는 모두 제거하는 경우다. 이럴 때는 정규표현식이 필수다.
// ✗ 잘못된 것: replaceAll()로 평문 문자열만 쓰면 불가능
const phone = "010-1234-5678";
// 숫자가 아닌 모든 것을 제거하고 싶은데, 평문으로는 불가능
// ✓ 올바른 것: 정규표현식으로 패턴 매칭
const digits = phone.replaceAll(/\D/g, "");
console.log(digits);
// 출력: "01012345678"
// \D는 숫자가 아닌 문자를 의미함
정규표현식 /\D/g는 숫자가 아닌 모든 문자를 찾는다. g 플래그가 없으면 TypeError가 발생한다는 점을 기억하자.
// ✗ 잘못된 것
const text = "apple apple";
const wrong = text.replaceAll(/apple/, "orange");
// TypeError: String.prototype.replaceAll called with a non-global RegExp argument
// ✓ 올바른 것
const right = text.replaceAll(/apple/g, "orange");
console.log(right);
// 출력: "orange orange"
단순 문자열 대신 함수를 전달하면 더 복잡한 로직을 구성할 수 있다. replaceAll()도 지원한다.
// replace(): 첫 번째 매칭만 함수 실행
const text = "apple apple";
const result1 = text.replace(/apple/g, (match) => {
return match.toUpperCase();
});
console.log(result1);
// 출력: "APPLE APPLE"
// 함수의 match, p1, p2... offset, string 파라미터 활용
const text2 = "Price: 100, 200, 300";
const result2 = text2.replaceAll(/(\d+)/g, (match, p1, offset, string) => {
return parseInt(p1) * 2; // 모든 숫자를 2배로
});
console.log(result2);
// 출력: "Price: 200, 400, 600"
정규표현식 메타문자(., +, *, ?, ^, $, |, \, (, ), [, ], {, })는 특수한 의미를 가진다. 문자 그 자체로 찾으려면 백슬래시로 이스케이프해야 한다.
// ✗ 잘못된 것: . 메타문자는 모든 문자를 의미
const url = "example.com";
const wrong = url.replaceAll(/./, "X");
console.log(wrong);
// 출력: "XXXXXXXXXXX" (모든 문자가 X로 바뀜)
// ✓ 올바른 것: 점 문자 자체를 찾으려면 이스케이프
const right = url.replaceAll(/\./g, "_");
console.log(right);
// 출력: "example_com"
원본 문자열을 수정하지 않는다. 반환값을 새로운 변수에 저장하거나 사용해야 한다.
// ✗ 잘못된 것: 원본 문자열이 바뀐다고 생각
let text = "apple";
text.replaceAll("apple", "orange"); // 결과를 저장하지 않음
console.log(text);
// 출력: "apple" (변경 안 됨)
// ✓ 올바른 것: 반환값을 저장
let text2 = "apple";
text2 = text2.replaceAll("apple", "orange");
console.log(text2);
// 출력: "orange"
IE 11 환자가 아직도 있다면 replaceAll() 대신 replace() + 정규표현식 g 플래그를 써야 한다. 또는 폴리필을 추가하거나 프로젝트에서 IE 지원을 종료하는 것도 고려하자.
// IE 11 호환 버전
const text = "apple apple apple";
const result = text.replace(/apple/g, "orange");
console.log(result);
// 출력: "orange orange orange"
실제로 성능 차이가 있을까? 간단한 벤치마크를 생각해보자. 매우 큰 문자열에서 만 개의 단어를 모두 바꿀 때 replace(/pattern/g)와 replaceAll()의 처리 속도는 사실상 같다. 둘 다 내부적으로 유사한 알고리즘을 사용하기 때문이다.
성능보다 중요한 것은 코드 가독성이다. 단순 문자열 치환이면 replaceAll()이 더 명확하고, 복잡한 패턴이면 정규표현식이 피할 수 없다. 현대 브라우저에서는 replaceAll()을 써도 무방하지만, 레거시 시스템이나 IE 지원이 필요하면 replace(/pattern/g) 형태를 써야 한다.
문자열 치환은 간단해 보이지만, replace()와 replaceAll()의 차이를 모르면 버그로 이어질 수 있다. replace()는 첫 번째만 바꾸고, replaceAll()은 모두 바꾼다. 단순 문자열이면 replaceAll()을 쓰고, 정규표현식이 필요하면 g 플래그를 반드시 붙여야 한다.
이 글의 실전 예제와 주의사항을 참고해서 URL 쿼리 파라미터, CSV 데이터, HTML 엔티티 같은 실무 상황에서 올바른 메서드를 선택하면, 간단하면서도 안전한 문자열 처리를 할 수 있을 것이다. 특히 replaceAll()을 체이닝으로 연속 사용할 때는 처리 순서가 결과에 영향을 미친다는 점을 잊지 말자.