웹 개발 배우기 11편 - 글자를 뒤집는 마법, 자바스크립트 Map 완전 정복
이 글은 프로그래밍 경험이 없는 분들을 대상으로 자바스크립트 웹 앱 제작법을 알려드리는 ‘웹 개발 배우기’ 시리즈의 일부인데요.
이번 시간에는 특정 입력값을 다른 출력값으로 ‘매핑(mapping)‘해주는 ‘맵(Map)‘이라는 자료 구조에 대해 알아볼 건데요.
이 Map을 활용해서 터미널에 텍스트를 거꾸로 출력하는 아주 재미있는 프로젝트도 함께 해볼 겁니다.
자바스크립트의 Map 클래스
‘맵(Map)‘은 ‘엔트리(entry)‘들의 모음인데요.
각 엔트리는 ‘키(key)‘와 ‘값(value)‘으로 이루어진 한 쌍입니다.
키를 이용해서 값을 찾아낸다는 점에서 마치 사전과 비슷하거든요.
예를 들어 영한사전처럼, 영어 단어를 주면 스페인어 단어를 찾아볼 수 있습니다.
그래서 이 자료 구조를 종종 ‘딕셔너리(dictionary)‘라고 부르기도 합니다.
Map 만들기
Map 클래스를 호출하여 Map을 만들었는데요.
그 인자는 키-값 쌍의 배열입니다.
각 쌍 또한 배열에 저장됩니다.
const englishToSpanish = new Map([
['yes', 'sí'],
['no', 'no'],
['maybe', 'quizás'],
]);
배열을 사용하는 것은 더 간결하다는 장점과 덜 설명적이라는 단점이 있습니다.
Map의 .size
Map은 자신의 엔트리 개수를 나타내는 .size 프로퍼티를 가지고 있거든요.
> englishToSpanish.size
3
배열은 그 요소들의 순서가 중요하기 때문에 .length를 가지고 있다는 점에 주목하세요.
반면에 Map에서는 엔트리의 순서가 중요하지 않습니다.
그래서 .size를 가지고 있는 겁니다.
값 가져오고 설정하기
Map 안에서는 하나의 키가 단 하나의 엔트리만 가질 수 있는데요.
.get() 메서드로 키에 해당하는 값을 찾아올 수 있고, .set() 메서드로 키에 연결된 값을 변경할 수 있습니다.
> englishToSpanish.get('maybe')
'quizás'
> englishToSpanish.set('maybe', 'tal vez');
> englishToSpanish.get('maybe')
'tal vez'
만약 존재하지 않는 키로 값을 찾으려고 하면 .get()은 undefined를 반환하는데요.
이때 .set()을 사용하면 새로운 엔트리를 추가할 수 있습니다.
> englishToSpanish.set('hello', 'hola');
> englishToSpanish.get('hello')
'hola'
키 존재 여부 확인하기
.has() 메서드는 특정 키가 Map에 존재하는지 확인할 수 있거든요.
> englishToSpanish.has('yes')
true
> englishToSpanish.has('goodbye')
false
키로서의 원시 값 vs. 객체
Map의 키로 객체를 사용할 수도 있는데요.
하지만 Map의 키 비교는 === 연산자와 비슷하게 동작하기 때문에, 새로운 빈 객체로는 위 Map의 값을 찾아올 수 없습니다.
따라서 객체는 드물게 좋은 Map 키가 되는데요 (아쉬운 점이죠).
하지만 동일한 객체를 가리키는 변수를 사용하면 값을 찾아올 수 있습니다.
import * as assert from 'node:assert/strict';
const key = {};
const objToStr = new Map([
[key, 'empty object'],
]);
assert.equal(
objToStr.get(key), 'empty object'
);
앞으로는 assert의 import 구문은 종종 생략할 테니 참고해주세요.
?? 연산자
자바스크립트는 결과값이 없을 때 종종 ‘값이 없음’을 의미하는 undefined를 사용하는데요.
이럴 때 ?? 연산자를 사용하면 undefined일 경우를 대비한 ‘기본값’을 아주 간단하게 지정할 수 있습니다.
> 'real value' ?? 'default'
'real value'
> undefined ?? 'default'
'default'
함수 안에서 활용하면 이런 모습이 됩니다.
> const useDefault = x => x ?? 'no value';
> useDefault(undefined)
'no value'
.get()은 키를 찾지 못하면 undefined를 반환하기 때문에 ?? 연산자와 함께 쓰면 아주 유용하거든요.
아래 코드는 사전에 단어가 없을 경우 (not found)를 반환합니다.
const lookUpWord = (word) => {
return englishToSpanish.get(word) ?? '(not found)'
};
assert.equal(lookUpWord('mountain'), '(not found)');
Map 활용 예제 문자 개수 세기
countChars() 함수는 문자열을 받아서 각 문자가 몇 번 등장했는지 세고, 그 결과를 Map으로 반환하는데요.
function countChars(chars) {
const charCounts = new Map();
for (let ch of chars) { // (A)
ch = ch.toLowerCase(); // (B)
const prevCount = charCounts.get(ch) ?? 0; // (C)
charCounts.set(ch, prevCount + 1);
}
return charCounts;
}
(A) for-of 반복문으로 문자열의 각 코드 포인트를 순회하고요.
(B) toLowerCase()를 사용해 ‘a’와 ‘A’를 같은 문자로 취급합니다.
(C) ?? 연산자를 사용해서 아직 등록되지 않은 문자의 이전 개수를 0으로 처리하는 것이 이 코드의 핵심입니다.
for 반복문
for-of 반복문과 이름은 비슷하지만, 실제로는 while 반복문과 더 가까운 for 반복문도 있는데요.
문법은 다음과 같습니다.
for (<선언>; <조건>; <액션>) {
// 본문
}
선언부는 반복문 시작 전에 한 번 실행되고, 조건부는 매 반복마다 체크되며, 액션은 매 반복이 끝난 후에 실행됩니다.
배열 메서드로 반복하기 .map()과 .filter()
배열에는 반복 작업을 아주 우아하게 처리해주는 여러 메서드들이 있는데요.
그중 .map()과 .filter()는 새로운 배열을 만들어 반환한다는 공통점이 있습니다.
array.map()
array.map()은 배열의 모든 요소를 하나씩 방문하면서, 인자로 받은 함수(콜백 함수)를 실행하고 그 결과를 모아 새로운 배열을 만드는데요.
> [1, 2, 3].map(num => num * num)
[ 1, 4, 9 ]
실무에서는 주로 객체 배열에서 특정 프로퍼티만 추출할 때 아주 유용하게 사용됩니다.
array.filter()
array.filter()는 이름 그대로 배열의 요소를 ‘필터링’하는 역할을 하는데요.
콜백 함수가 true를 반환하는 요소들만 모아서 새로운 배열을 만듭니다.
> [0, -1, 4, -8, 7].filter(x => x < 0)
[ -1, -8 ]
이름이 같아요 Map vs. .map()
자료 구조 Map과 배열 메서드 .map()은 이름이 같아서 헷갈릴 수 있는데요.
둘 다 무언가를 다른 무언가로 ‘매핑’한다는 공통점이 있지만, 전혀 다른 개념이니 주의해야 합니다.
노드제이에스 셸 인자 다루기
전역 변수인 process 객체의 argv 프로퍼티는 현재 셸 명령어와 그 인자들을 배열 형태로 담고 있거든요.
process-argv.js 파일을 만들고 아래 코드를 작성한 뒤, 터미널에서 node process-argv.js first "two words"와 같이 실행해 보세요.
console.log(process.argv);
결과는 다음과 비슷할 겁니다.
[
'/usr/bin/node',
'/home/robin/learning-web-dev-code/projects/process-argv.js',
'first',
'two words'
]
인덱스 0에는 노드제이에스 실행 파일의 경로가, 인덱스 1에는 실행한 스크립트 파일의 경로가 들어있고요.
그 이후부터 우리가 전달한 인자들이 순서대로 들어있는 것을 확인할 수 있습니다.
프로젝트 upside-down-text.js
드디어 오늘의 최종 프로젝트입니다.
이 프로젝트는 입력받은 텍스트를 거꾸로 뒤집어진 유니코드 문자로 변환해서 출력하는데요.
node upside-down-text.js "How are you?"
출력 결과는 다음과 같습니다.
¿noʎ ǝɹɐ ʍoH
문자 변환을 위한 Map 설정하기
먼저 일반 문자를 뒤집어진 문자로 변환하기 위한 Map을 만들어야 하는데요.
const dict = new Map();
const addToDict = (source, target) => {
const srcArr = Array.from(source);
const trgArr = Array.from(target);
if (srcArr.length !== trgArr.length) { // (A)
throw new Error();
}
for (let i = 0; i < srcArr.length; i++) {
dict.set(srcArr[i], trgArr[i]);
}
};
addToDict(`ABCDEFGHIJKLMNOPQRSTUVWXYZ`, `ⱯꓭƆꓷƎℲ⅁HIꓩꓘ⅂ꟽNOԀꝹꓤS⊥ՈɅ𐤵X⅄Z`);
// ... 소문자, 숫자, 특수문자 추가 ...
(A)에서 우리는 안전 검사를 수행하고 실패하면 오류를 던집니다.
거꾸로 된 문자열 만들기
이제 dict를 사용해서 실제로 문자열을 뒤집는 함수를 만들어 보겠습니다.
const upsideDown = (str) => {
return Array.from(str)
.reverse()
.map(
(srcChar) => dict.get(srcChar) ?? srcChar
).join('');
};
코드는 다음과 같은 단계를 거칩니다.
-
문자열을 코드 유닛 배열로 변환합니다.
-
.reverse()메서드로 배열의 순서를 뒤집습니다. -
.map()메서드로 배열의 각 문자를dict를 이용해 뒤집어진 문자로 변환합니다. -
.join('')메서드로 배열을 다시 하나의 문자열로 합칩니다.
셸 입출력 처리하기
마지막으로, 셸로부터 입력을 받고 처리 결과를 출력하는 코드를 추가하면 프로젝트가 완성됩니다.
const arg = process.argv[2]; // (A)
console.log(upsideDown(arg)); // (B)