새로 삽입한 텍스트를 바꾸지 않고 치환 맵을 적용하려면 원본 문자열만 검색하고 별도의 결과를 만드세요. 이 가이드에서는 Mac의 Safari에서 JavaScript를 사용해 짧은 문단을 예측 가능한 리터럴 치환으로 수정합니다.
데스크톱 편집 작업에서 blue을 green으로, green를 gold로 바꿔야 한다고 가정해 보세요. 원본의 blue은 한 번만 green가 되어야 합니다. 원본의 green은 gold이 되어야 합니다. 다음 접근법은 이러한 차이와 겹치는 구문, 인용된 텍스트, 문장 부호가 포함된 치환 값을 처리합니다.
치환 루프가 텍스트를 두 번 바꾸는 이유는 무엇인가요?
각 규칙을 이전 규칙의 출력에 적용하는 루프는 연쇄 치환을 일으킵니다. 두 색상 규칙을 적용하면 첫 번째 작업에서 green이 삽입되고, 두 번째 작업은 새로 삽입된 단어를 찾아 gold로 바꿉니다. 규칙 순서를 바꾸면 한 사례는 해결될 수 있지만, 모든 맵에서 안정적으로 작동한다고 보장할 수는 없습니다.
JavaScript의 replace()은 새 문자열을 반환하고 입력은 그대로 둡니다. 이후 호출에 반환된 문자열을 전달하면 연쇄 치환이 발생합니다. 따라서 작업에 명확한 규칙이 필요합니다. 모든 일치 항목은 원본 입력에서 찾아야 합니다. MDN의 String.prototype.replace() 문서를 참조하세요.
작성된 입력이 The blue cup is beside the green cup.일 때 의도한 결과는 The green cup is beside the gold cup.입니다. 원본에 있던 두 색상 단어는 모두 바뀌고, 삽입된 green은 그대로 유지됩니다. 아래 구현은 이 변환을 수행합니다.
치환 키가 겹치면 어떻게 해야 하나요?
연쇄 치환을 막고 나면 다음과 같은 결정이 남습니다. 여러 키가 같은 원본 텍스트에 일치할 때 어떤 키를 우선할까요? 이 편집 작업에서는 다음과 같은 명시적 정책을 사용합니다. 이는 예시를 위해 정한 설계 선택이며, JavaScript가 자동으로 제공하는 규칙은 아닙니다.
- 원본 입력을 왼쪽에서 오른쪽으로 훑습니다.
- 각 위치에서 일치하는 키 중 가장 긴 것을 선택합니다.
- 계속 진행하기 전에 일치한 원본 전체를 소비합니다.
- 매핑된 값을 그 값 자체는 검색하지 않고 리터럴로 삽입합니다.
- 일치하지 않는 텍스트는 그대로 복사합니다.
- 대소문자와 공백을 정확히 구분합니다.
예를 들어 blue와 blue vase가 모두 키라면 같은 시작 위치에서 blue vase 구문이 우선합니다. 더 일찍 시작하는 일치 항목은 나중에 시작하는 더 긴 일치 항목보다 우선합니다. 원본 범위를 한 번 소비하면 겹치는 일치 항목이 그 범위를 다시 사용할 수 없습니다.
이 정책은 작은 리터럴 구문 편집에 적합합니다. 구체적인 구문이 더 짧은 접두어보다 우선합니다. 또한 길이가 다른 키를 입력한 순서와 관계없이 결과가 동일해집니다. 다른 겹침 정책이 필요하다면 맵을 적용하기 전에 정하세요.
Mac에서 변환을 실행하려면 어떻게 해야 하나요?
Safari에는 JavaScript 콘솔이 있으므로 이 예시를 위해 별도 앱을 설치할 필요가 없습니다. Safari → 설정… → 고급 → 웹 개발자용 기능 보기에서 Web Inspector를 활성화하세요. 그런 다음 개발자용 → 웹 검사기 보기를 열거나 Option-Command-I를 누르세요. 이 단계는 WebKit의 Web Inspector 설정 문서를 따릅니다.
Safari의 주소 필드에 about:blank를 입력해 빈 페이지를 연 다음, 검사기를 열고 Console 탭을 선택하세요. 아래 프로그램 전체를 콘솔 프롬프트에 붙여넣고 실행하세요. WebKit 문서에 따르면 프롬프트는 JavaScript를 실행하고 그 결과를 콘솔에 표시합니다. Web Inspector 인터페이스를 참조하세요.
이 예시는 합성 텍스트를 사용하는 완전한 브라우저 프로그램이며 가져오기를 사용하지 않습니다. 바깥 함수 안에 선언을 두어 샘플을 수정한 뒤 블록을 다시 붙여넣고 실행해도 선언이 지역 범위에 머뭅니다.
(() => {
function applyReplacementMap(input, replacements) {
if (typeof input !== "string" || !(replacements instanceof Map)) {
throw new TypeError("Use a string and a Map.");
}
const entries = [...replacements];
for (const [key, value] of entries) {
if (
typeof key !== "string" ||
key.length === 0 ||
typeof value !== "string"
) {
throw new TypeError(
"Keys must be nonempty strings; values must be strings."
);
}
}
entries.sort((a, b) => b[0].length - a[0].length);
const output = [];
let position = 0;
while (position < input.length) {
const entry = entries.find(([key]) =>
input.startsWith(key, position)
);
if (entry) {
output.push(entry[1]);
position += entry[0].length;
} else {
output.push(input.slice(position, position + 1));
position += 1;
}
}
return output.join("");
}
const replacements = new Map([
["blue", "green"],
["green", "gold"],
["blue vase", "green bowl"],
["[tag]", "$&"],
["(draft)", ""]
]);
const input =
'The blue vase is beside the green cup. Label: "[tag]". Note: (draft)';
const result = applyReplacementMap(input, replacements);
console.log(result);
})();
아래에 기록된 텍스트가 표시됩니다. Note: 뒤에는 끝 공백이 하나 남습니다. 규칙이 (draft)만 제거하고 그 앞의 공백은 보존하기 때문입니다.
The green bowl is beside the gold cup. Label: \"$&\". Note:
예시를 수정하려면 input와 new Map(...) 안의 쌍을 편집하세요. 각 쌍은 정확한 원본 문자열과 그에 대응하는 치환 문자열로 구성됩니다. 키마다 항목 하나만 사용하세요. JavaScript 맵은 고유한 키를 보유하므로 같은 키를 반복하면 서로 경쟁하는 규칙이 추가되는 대신 값이 갱신됩니다. MDN의 Map 참조 문서를 확인하세요.
삽입된 텍스트가 그대로 유지되는 이유
position이라는 이름의 커서는 input 안에서만 앞으로 이동합니다. 매칭에는 input.startsWith(key, position)을 사용합니다. 이 메서드는 지정된 위치에서 문자열이 시작되는지 확인하며, MDN의 startsWith() 참조 문서에 설명된 대로 대소문자를 구분합니다. 출력 배열을 검색 대상으로 사용하지 않습니다.
어떤 규칙도 일치하지 않으면 slice(position, position + 1)이 다음 원본 단위를 복사합니다. 두 번째 슬라이스 인덱스는 포함되지 않으며 슬라이싱은 원본 문자열을 수정하지 않습니다. 문서화된 이 동작 덕분에 함수는 원본과 별도로 결과를 만들면서 일치하지 않는 내용을 보존할 수 있습니다. MDN의 slice() 참조 문서를 확인하세요.
예시에서는 blue vase를 green bowl로 바꿉니다. 삽입된 이 구문에 또 다른 키인 green이 포함되어 있어도 함수는 이미 원본 구문을 지나쳤습니다. green bowl은 검사하지 않습니다. 입력 뒤쪽에 원래 있던 green은 여전히 gold로 바뀝니다.
리터럴 값과 삭제 처리 방식
이 구현은 치환 값을 직접 덧붙입니다. 따라서 $&는 $& 그대로 남고, 키에 들어 있는 대괄호나 괄호도 일반 문자로 취급됩니다. 빈 치환 문자열을 지정하면 일치한 원본 범위가 삭제됩니다. 이 함수에서는 정규식이나 치환 템플릿 해석이 일어나지 않습니다.
빈 키는 원본 텍스트를 소비하지 않으므로 거부합니다. 이를 허용하면 일치한 분기에서 앞으로 진행할 수 없습니다. 문자열이 아닌 값도 변환을 시작하기 전에 거부하므로, 잘못된 값이 자동으로 변환되거나 누락되는 대신 오류가 드러납니다. 빈 맵을 사용하면 입력을 그대로 반환하고, 입력이 비어 있으면 빈 문자열을 반환합니다.
복사하기 전에 결과를 어떻게 확인해야 하나요?
원하는 동작을 구분해 보여 주는 작은 사례를 사용하세요. 다음 예상 결과는 위 구현에서 도출했습니다. 별도 표시가 없는 한 예시의 샘플 맵을 사용합니다.
| 확인 항목 | 입력 | 예상 결과 |
|---|---|---|
| 삽입된 텍스트 유지 | blue green |
green gold |
| 긴 구문 우선 | blue vase |
green bowl |
| 치환 문장 부호를 리터럴로 유지 | [tag] |
$& |
| 대소문자 구분 | Blue |
Blue |
| 부분 문자열도 매칭 | blueprint |
greenprint |
| 따옴표 유지 | \"blue\" |
\"green\" |
| 빈 맵 | blue |
blue |
치환 함수와 이러한 동작을 삭제, 빈 입력, 겹치는 일치 항목, 잘못된 맵 거부 사례와 함께 Node.js 20.19.2에서 확인했습니다. 이는 해당 런타임에서 JavaScript 변환을 검증한 것이며, Safari 인터페이스 단계는 WebKit 문서를 참고했습니다.
부분 문자열 확인에 특히 주의하세요. 이 함수는 단어 경계를 추론하거나 문법을 교정하지 않습니다. 키가 더 긴 단어 안에서 일치할 수 있고, 텍스트를 제거하면 공백이 더 남을 수 있습니다. 구문 편집 작업에서는 주변 문자를 살펴보고 의도한 변경에 맞게 맵을 구체적으로 지정하세요.
또한 한 번 적용하는 것과 반복 적용하는 것을 구분하세요. 원본 텍스트에 함수를 한 번 적용하면 그 실행 중에는 연쇄 치환이 발생하지 않습니다. 결과를 다시 함수에 입력하면 새 변환이 시작되므로 앞서 삽입한 단어도 일치할 수 있습니다. 편집을 다시 실행할 때는 원본 input을 소스로 사용하세요.
JavaScript의 replace 호출 하나로 같은 작업을 할 수 있나요?
네. 단순한 키로 이루어진 작고 고정된 집합이라면 전역 정규식 하나와 치환 콜백으로 같은 비연쇄 동작을 구현할 수 있습니다. ECMAScript 명세는 원본 문자열에서 일치 항목을 모은 다음 치환 결과를 구성합니다. 명세에 정의된 정규식 치환 알고리즘을 참조하세요.
이 별도의 완전한 예시는 고정된 두 색상 키를 처리합니다.
(() => {
const replacements = new Map([
["blue", "green"],
["green", "gold"]
]);
const input = "The blue cup is beside the green cup.";
const result = input.replace(
/blue|green/g,
(word) => replacements.get(word)
);
console.log(result);
// The green cup is beside the gold cup.
})();
g 플래그는 정규식이 모든 일치 항목을 치환하도록 합니다. 콜백에서 값을 반환하면 $& 같은 치환 문자열 패턴도 방지할 수 있습니다. 그렇지 않으면 $&는 일치한 부분 문자열을 의미합니다. MDN은 replace() 참조 문서에서 두 동작을 모두 명시적으로 설명합니다.
문장 부호가 포함된 가변 맵에는 정규식을 구성할 때 추가 처리가 필요합니다. 키를 리터럴 패턴으로 이스케이프해야 하고 겹치는 대안에는 명시적인 우선순위를 정해야 합니다. JavaScript는 리터럴 패턴 구성을 위해 RegExp.escape()을 제공하지만, MDN은 구형 브라우저에서 지원되지 않을 수 있다고 설명합니다. MDN의 RegExp.escape() 문서를 참조하세요.
여기서 다루는 데스크톱 구문 편집 작업에서는 리터럴 스캐너를 사용하면 이런 결정이 명확하게 드러납니다. 원본 텍스트를 읽고, 각 위치에서 가장 긴 키를 선택하고, 각 치환 값을 그대로 덧붙입니다. 원본 문단에 적용하고 결과 문구와 공백을 확인한 다음 완성된 텍스트를 문서에 복사하세요.
출처
- Web Inspector 활성화 — WebKit.
- Web Inspector 인터페이스 — WebKit.
- String.prototype.replace() — MDN Web Docs, Mozilla.
- Map — MDN Web Docs, Mozilla.
- String.prototype.startsWith() — MDN Web Docs, Mozilla.
- String.prototype.slice() — MDN Web Docs, Mozilla.
- ECMAScript 언어 명세: RegExp.prototype [ %Symbol.replace% ] — Ecma International, TC39.
- RegExp.escape() — MDN Web Docs, Mozilla.