Hash, Map, Set
제가 맡은 주제는 Hash, Map, Set 입니다.
셋은 "값을 빨리 찾는다"는 한 가지 목적으로 이어져 있습니다. Hash 는 빨리 찾는 원리이고, Map 과 Set 은 그 원리로 만든 JavaScript 자료구조입니다.
빠르다는 걸 설명하려면 기준이 필요해서, 자료구조와 시간복잡도를 먼저 짚고 Hash, Map, Set 순서로 갑니다. 마지막에 실무 예제와 비교표가 있습니다.
1. 자료구조란?
자료구조는 데이터를 담는 방식입니다.
같은 데이터라도 어떻게 담느냐에 따라 꺼내는 방법과 속도가 달라집니다. 매일 쓰는 배열과 객체도 자료구조입니다.
사용자 5명을 두 가지 방식으로 담아 봤습니다.
// 배열: 순서대로 담고, 위치 번호(인덱스)로 꺼냄
const users = [
{ id: 3, name: "민수" },
{ id: 12, name: "지은" },
{ id: 5, name: "현우" },
{ id: 7, name: "서연" },
{ id: 9, name: "도윤" },
];
users[3]; // { id: 7, name: "서연" }
// 객체: 이름표(키)를 붙여 담고, 키로 꺼냄
const usersById = {
3: { id: 3, name: "민수" },
5: { id: 5, name: "현우" },
7: { id: 7, name: "서연" },
9: { id: 9, name: "도윤" },
12: { id: 12, name: "지은" },
};
usersById[7]; // { id: 7, name: "서연" }
여기서 id 가 7 인 사용자를 찾는다고 해 보겠습니다.
배열은 id 7 이 몇 번째 칸에 있는지 모르니 앞에서부터 하나씩 확인합니다.
객체는 키 7 로 바로 꺼냅니다. 몇 번째에 있는지 알 필요가 없습니다.
| 배열 | 객체 | |
|---|---|---|
| 담는 방식 | 순서대로 | 키와 값을 쌍으로 |
| 꺼내는 기준 | 위치 번호(인덱스) | 키 |
| 값으로 찾기 | 앞에서부터 하나씩 (find, includes) | 키를 알면 바로 |
| 잘 맞는 데이터 | 목록, 순서가 중요한 것 | 이름으로 찾는 것, 필드가 정해진 것 |
id 로 찾는 일이 많으면 객체가 유리합니다. 얼마나 유리한지를 숫자로 말하는 방법이 다음 절의 시간복잡도입니다.
2. 시간복잡도와 공간복잡도
2.1 시간복잡도: 데이터가 늘면 몇 번 더 일하나
시간복잡도는 데이터 개수가 늘어날 때 작업 횟수가 어떻게 늘어나는지를 나타냅니다.
실제 걸리는 시간(ms)은 컴퓨터마다 달라서 비교 기준이 되지 못합니다. 그래서 몇 번 확인하는지로 셉니다.
데이터 개수를 n 이라고 부르고, O(n) 처럼 O 괄호 안에 적습니다. 이 표기를 빅오(Big-O) 표기법이라고 합니다.
O(n) 은 "오 엔" 또는 "빅오 엔"이라고 읽습니다. 영어로는 "Big O of n" 이고, 줄여서 "O of n" 이라고도 합니다.
1장의 두 방식에서 확인 횟수를 사용자 수별로 세면 이렇습니다.
| 사용자 수 (n) | 배열 find 최대 확인 횟수 | 객체 usersById[7] |
|---|---|---|
| 10 | 10번 | 1번 |
| 1,000 | 1,000번 | 1번 |
| 1,000,000 | 1,000,000번 | 1번 |
배열은 찾는 사용자가 맨 끝에 있거나 아예 없으면 n 번을 다 확인합니다. 데이터에 비례해서 늘어나니 O(n) 입니다.
객체는 사용자가 100만 명이어도 한 번에 꺼냅니다. 데이터 개수와 상관없으니 O(1) 입니다.
자주 보는 세 가지를 그래프로 그리면 이렇습니다.
| 표기 | 읽는 법 | 이름 | 뜻 | 예시 |
|---|---|---|---|---|
O(1) | 오 원 | 상수 시간 | 데이터가 늘어도 횟수 그대로 | arr[3], obj.key, arr.push() |
O(n) | 오 엔 | 선형 시간 | 데이터만큼 늘어남 | find, includes, for 한 바퀴 |
O(n²) | 오 엔 제곱 | 제곱 시간 | 데이터의 제곱만큼 늘어남 | for 안에 for |
O(n²) 은 반복문 안에서 또 한 바퀴를 돌 때 나옵니다. 1,000개면 1,000 × 1,000 = 100만 번입니다.
반복문 안에서 find 나 includes 를 부르는 코드도 O(n²) 입니다. 코드는 한 줄이지만 이 메서드들이 안에서 한 바퀴를 돌기 때문입니다.
빅오는 늘어나는 모양만 봅니다. 2n + 3 번이든 n 번이든 데이터에 비례한다는 모양이 같아서 둘 다 O(n) 으로 씁니다.
2.2 공간복잡도: 메모리를 얼마나 더 쓰나
공간복잡도는 같은 방식으로 메모리를 셉니다. 작업에 메모리가 얼마나 더 필요한지를 데이터 개수 기준으로 나타냅니다.
1장의 usersById 는 공짜가 아닙니다. 배열에서 객체를 만들려면 사용자 수만큼 칸이 더 필요합니다.
const usersById = {};
for (const user of users) {
usersById[user.id] = user; // user 객체는 복사하지 않고 참조만 담지만, 칸은 n 개 필요. 공간 O(n)
}
만드는 데 한 바퀴(O(n))가 들지만 한 번이면 됩니다. 그 뒤로는 찾을 때마다 O(1) 입니다.
메모리를 조금 더 써서 찾는 시간을 크게 줄이는 거래이고, 해시를 쓰는 이유가 이 거래입니다.
화면에서 다루는 데이터는 대부분 수백, 수천 개라 평소에는 차이가 잘 안 느껴집니다. 차이가 나는 곳은 렌더링마다 도는 반복문 안에서 찾기를 반복하는 코드입니다.
3. Hash
3.1 Hash 와 Hash Table
usersById[7] 은 100만 명 중에서도 어떻게 한 번에 찾을까요?
키를 칸 번호로 바꾸는 계산이 있기 때문입니다. 이 계산을 해시 함수(hash function)라고 하고, 계산 결과를 해시값이라고 합니다.
해시 테이블은 칸이 여러 개인 배열에, 해시 함수가 정해 준 칸 번호로 값을 넣어 두는 자료구조입니다.
해시 함수를 아주 단순하게 흉내 내면 이렇습니다.
// 설명용. 실제 엔진의 해시 함수는 키가 칸에 고르게 퍼지도록 훨씬 정교하게 만듦
const hash = (key, size) => {
let sum = 0;
for (const char of key) sum += char.charCodeAt(0); // 글자 코드를 모두 더함
return sum % size; // 칸 수로 나눈 나머지가 칸 번호
};
hash("audi", 8); // 3
hash("ford", 8); // 3
hash("kia", 8); // 5
hash("bmw", 8); // 6
이 함수로 키 네 개를 칸 8개짜리 테이블에 넣으면 이렇게 됩니다.
저장할 때는 키를 해시 함수에 넣어 칸 번호를 얻고, 그 칸에 값을 넣습니다.
찾을 때도 같은 키를 넣으면 같은 칸 번호가 나오니까 그 칸만 보면 됩니다. 테이블에 키가 몇 개 있든 계산 한 번, 칸 하나라서 O(1) 입니다.
배열은 위치 번호를 알면 바로 꺼낼 수 있었습니다. 해시 테이블은 키를 위치 번호로 바꿔서 배열의 이 장점을 키에도 쓰는 구조입니다.
3.2 충돌
그림에서 audi 와 ford 는 칸 번호가 둘 다 3 입니다. 이렇게 다른 키가 같은 칸 번호를 받는 것을 충돌(collision)이라고 합니다.
칸 수는 정해져 있는데 키는 무엇이든 들어올 수 있어서 충돌은 피할 수 없습니다.
그래서 칸에는 값만이 아니라 키도 같이 저장하고, 한 칸에 여러 개가 오면 줄을 세워 둡니다. ford 를 찾을 때는 3번 칸에 가서 audi 와 ford 의 키를 하나씩 비교합니다.
충돌이 적으면 칸마다 한두 개라 O(1) 에 가깝습니다. 모든 키가 한 칸에 몰리면 결국 하나씩 확인하는 O(n) 이 됩니다.
그래서 해시 테이블의 O(1) 에는 "평균적으로"라는 말이 붙습니다. 실제 엔진은 키를 고르게 흩는 해시 함수를 쓰고, 칸이 차면 칸 수를 늘려 다시 배치해서 평균 O(1) 을 지킵니다.
3.3 JavaScript 의 해시 테이블
해시 테이블을 직접 만들 일은 거의 없고, 언어가 주는 것을 씁니다.
ECMAScript 명세는 Map 과 Set 을 해시 테이블 또는 평균 접근 시간이 요소 수에 비례하지 않는 다른 방식으로 구현하라고 정해 두었습니다. V8 같은 엔진은 실제로 해시 테이블을 씁니다.
객체도 키로 값을 찾으니 해시 테이블처럼 쓸 수 있습니다. 다만 객체는 필드가 정해진 데이터를 담으려고 만든 것이라, 키를 계속 넣고 빼는 용도로 쓰면 불편한 점이 생깁니다.
그 빈자리를 채우려고 ES2015 에 들어온 것이 Map 과 Set 입니다.
| 저장하는 것 | 쓰는 때 | |
|---|---|---|
| Map | 키와 값의 쌍 | 키로 값을 찾을 때 |
| Set | 값 (키 = 값) | 값이 있는지만 확인할 때 |
4. Map
4.1 Map 의 개념과 특징
Map 은 키와 값을 쌍으로 저장하는 자료구조입니다. 객체와 하는 일은 비슷하지만 키를 다루는 방식이 다릅니다.
const stock = new Map();
stock.set("bmw", 3); // 추가
stock.set("benz", 5);
stock.get("bmw"); // 3
stock.has("kia"); // false
stock.set("bmw", 4); // 같은 키면 덮어씀
stock.delete("benz"); // true
stock.size; // 1
// [키, 값] 배열로 한 번에 만들 수도 있음
const stock2 = new Map([
["bmw", 3],
["benz", 5],
]);
4.2 Object 와 차이점
키 타입이 다릅니다. 객체는 키를 문자열로 바꾸고, Map 은 넣은 값을 그대로 키로 씁니다.
const obj = {};
obj[1] = "숫자 1";
obj["1"] = "문자 1";
Object.keys(obj); // ["1"]. 숫자 1 도 "1" 로 바뀌어서 덮어씀
const map = new Map();
map.set(1, "숫자 1");
map.set("1", "문자 1");
map.size; // 2. 숫자 1 과 문자 "1" 은 다른 키
const user = { id: 7 };
obj[user] = "서연"; // 키가 "[object Object]" 라는 문자열이 됨
map.set(user, "서연"); // user 객체 자체가 키
빈 객체에도 키가 있습니다. 객체는 프로토타입에서 물려받은 키를 갖고 시작합니다.
const counts = {};
"constructor" in counts; // true. 아무것도 안 넣었는데 있음
const countMap = new Map();
countMap.has("constructor"); // false
사용자가 입력한 태그 개수를 세는 코드에 "constructor" 라는 태그가 들어오면 객체는 엉뚱한 값을 냅니다. Object.create(null) 로 피할 수 있지만, Map 은 처음부터 이 문제가 없습니다.
키 순서가 다릅니다. 객체는 정수처럼 생긴 키를 앞으로 정렬하고, Map 은 넣은 순서를 지킵니다.
const obj = { b: "B", 2: "둘", a: "A", 1: "하나" };
Object.keys(obj); // ["1", "2", "b", "a"]
const map = new Map([
["b", "B"],
[2, "둘"],
["a", "A"],
[1, "하나"],
]);
[...map.keys()]; // ["b", 2, "a", 1]
나머지 차이는 표로 모았습니다.
| Object | Map | |
|---|---|---|
| 키 타입 | 문자열, Symbol (숫자는 문자열로 바뀜) | 모든 값 (객체, 함수 포함) |
| 기본 키 | 프로토타입에서 물려받은 키 있음 | 없음 |
| 순서 | 정수 키가 먼저 오름차순, 나머지는 넣은 순서 | 넣은 순서 |
| 크기 | Object.keys(obj).length | map.size |
| 순회 | Object.entries(obj) 로 바꿔서 | for...of 로 바로 |
| 잦은 추가·삭제 | 이 용도로 최적화되어 있지 않음 | 이 용도로 최적화 |
| JSON 변환 | JSON.stringify 로 바로 | 안 됨 ("{}" 가 나옴) |
API 요청·응답처럼 필드가 정해진 데이터는 객체가 맞고, 실행 중에 키가 계속 바뀌는 데이터는 Map 이 맞습니다.
Map 을 서버로 보내거나 localStorage 에 저장할 때는 객체로 바꿔야 합니다.
JSON.stringify(stock); // "{}". 내용이 사라짐
JSON.stringify(Object.fromEntries(stock)); // '{"bmw":4}'
4.3 객체를 키로 쓸 때 주의점
Map 은 객체 키를 내용이 아니라 참조로 비교합니다. 모양이 같아도 다른 객체면 다른 키입니다.
const map = new Map();
map.set({ id: 7 }, "서연");
map.get({ id: 7 }); // undefined. 모양만 같은 새 객체라서
const key = { id: 7 };
map.set(key, "서연");
map.get(key); // "서연". 같은 객체를 넘겨야 찾음
API 를 다시 호출하면 내용이 같아도 새 객체가 만들어집니다. 그래서 서버 데이터는 객체 대신 id 같은 원시값을 키로 씁니다.
4.4 주요 메서드와 사용 사례
| 메서드·속성 | 하는 일 |
|---|---|
set(key, value) | 추가. 같은 키면 덮어씀. Map 을 반환해서 이어 쓸 수 있음 |
get(key) | 값 꺼내기. 없으면 undefined |
has(key) | 키가 있는지 확인 |
delete(key) | 삭제. 지웠으면 true |
clear() | 전부 삭제 |
size | 개수 (메서드가 아니라 속성) |
keys(), values(), entries() | 순회용 이터레이터 |
set, get, has, delete 는 모두 평균 O(1) 입니다.
자주 쓰는 곳은 id 로 찾기, API 결과 캐싱, 개수 세기입니다. 개수 세기는 이렇게 씁니다.
const brands = ["bmw", "benz", "bmw", "kia", "bmw"];
const countByBrand = new Map();
for (const brand of brands) {
countByBrand.set(brand, (countByBrand.get(brand) ?? 0) + 1); // 처음 보는 키면 0 부터
}
// Map { "bmw" => 3, "benz" => 1, "kia" => 1 }
id 로 찾기와 캐싱은 6장에 실무 코드로 있습니다.
5. Set
5.1 Set 의 개념과 특징
Set 은 값만 저장하고, 같은 값은 한 번만 들어가는 자료구조입니다. Map 에서 값을 빼고 키만 남긴 모양입니다.
const tags = new Set();
tags.add("bmw");
tags.add("benz");
tags.add("bmw"); // 이미 있으니 무시
tags.size; // 2
tags.has("bmw"); // true
tags.delete("benz"); // true
5.2 Array 와 차이점
| Array | Set | |
|---|---|---|
| 중복 | 허용 | 허용 안 함 |
| 순서 | 있음. 인덱스로 접근 (arr[0]) | 넣은 순서로 순회는 되지만 인덱스 없음 |
| 있는지 확인 | includes() · O(n) | has() · 평균 O(1) |
| 값 지우기 | 위치를 찾아서 splice · O(n) | delete() · 평균 O(1) |
| 배열 메서드 | map, filter, sort 등 | 없음. 배열로 바꿔서 사용 |
includes 를 반복문 안에서 쓰면 차이가 커집니다. 검색 결과 중 관심 매물만 고르는 코드입니다.
const favoriteIds = [/* 관심 매물 id 1,000개 */];
const items = [/* 검색 결과 1,000개 */];
// items 한 바퀴마다 favoriteIds 를 또 한 바퀴. 1,000 × 1,000 = 100만 번
items.filter((item) => favoriteIds.includes(item.id));
// Set 을 한 번 만들고(1,000번), 찾을 때마다 1번. 약 2,000번
const favoriteSet = new Set(favoriteIds);
items.filter((item) => favoriteSet.has(item.id));
favoriteSet 이 관심 매물 수만큼 메모리를 더 쓰는 대신(공간 O(n)) 시간은 O(n²) 에서 O(n) 으로 줄어듭니다. 2.2절의 거래와 같습니다.
5.3 중복 제거
Set 을 가장 많이 쓰는 곳입니다.
const brands = ["bmw", "benz", "bmw", "kia", "benz"];
const unique = [...new Set(brands)]; // ["bmw", "benz", "kia"]
배열을 Set 에 넣으면 중복이 빠지고, 스프레드 [...] 로 다시 배열로 바꿉니다. 순서는 처음 들어온 순서를 따릅니다.
배열만으로도 할 수 있지만 느립니다.
// filter 한 바퀴 안에서 indexOf 가 또 한 바퀴. O(n²)
brands.filter((brand, i) => brands.indexOf(brand) === i);
// Set 에 넣으며 한 바퀴. O(n)
[...new Set(brands)];
Set 도 객체는 참조로 비교합니다. 내용이 같은 객체 두 개는 둘 다 남습니다.
new Set([{ id: 1 }, { id: 1 }]).size; // 2
객체 배열은 id 같은 원시값을 기준으로 중복을 걸러야 하고, 그 코드는 6.2절에 있습니다.
조금 더 · 합집합, 교집합, 차집합 메서드
ES2025 에 Set 끼리 비교하는 메서드가 추가됐습니다.
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
a.union(b); // Set {1, 2, 3, 4}
a.intersection(b); // Set {2, 3}
a.difference(b); // Set {1}
a.isSubsetOf(b); // false
Chrome 122, Firefox 127, Safari 17 부터 지원합니다. 더 오래된 브라우저를 지원해야 하면 5.2절의 filter + has 방식을 씁니다.
6. 실무 활용 예제
6.1 Map 으로 데이터 조회와 캐싱
id 로 조회하기. 장바구니에는 매물 id 만 있고, 매물 정보는 목록 API 에서 받는 화면입니다.
interface Item {
id: number;
name: string;
}
interface CartListProps {
items: Item[];
cartIds: number[];
}
export default function CartList({ items, cartIds }: CartListProps) {
// items 가 바뀔 때만 다시 만듦
const itemById = useMemo(
() => new Map(items.map((item) => [item.id, item])),
[items],
);
return (
<ul>
{cartIds.map((id) => {
const item = itemById.get(id); // find 였다면 장바구니 한 칸마다 목록을 한 바퀴
return item ? <li key={id}>{item.name}</li> : null;
})}
</ul>
);
}
items.find 로 찾으면 장바구니 m 개 × 목록 n 개만큼 확인합니다. Map 을 만들면 만드는 데 n 번, 찾는 데 m 번이라 O(n + m) 입니다.
useMemo 가 없어도 find 보다는 낫습니다. 다만 목록이 그대로인데 렌더링마다 Map 을 다시 만들 이유는 없습니다.
요청 결과 캐싱하기. 같은 키워드로 다시 검색하면 요청을 보내지 않고 저장해 둔 결과를 씁니다.
const cache = new Map<string, Promise<Item[]>>();
export const getItems = (keyword: string) => {
const cached = cache.get(keyword);
if (cached) return cached;
const request = api
.get<Item[]>("/items", { params: { keyword } })
.then((res) => res.data)
.catch((error) => {
cache.delete(keyword); // 실패한 Promise 가 남으면 그 키워드는 영영 실패만 돌려줌
throw error;
});
cache.set(keyword, request); // 응답이 아니라 Promise 를 저장해서 동시 호출도 요청 한 번
return request;
};
응답 대신 Promise 를 저장하는 데는 이유가 있습니다. 응답이 오기 전에 같은 키워드로 두 번 불러도, 두 번째 호출은 진행 중인 Promise 를 같이 기다리기 때문에 요청이 한 번만 나갑니다.
이 Map 은 커지기만 합니다. Map 은 넣은 순서를 기억하므로 cache.keys().next().value 로 가장 오래된 키를 꺼내 지우면 개수 제한을 걸 수 있습니다.
만료 시간이나 재요청 시점까지 직접 챙기기는 번거로워서 실제 화면에서는 TanStack Query 를 씁니다. TanStack Query 의 QueryCache 도 queryKey 를 문자열로 바꾼 queryHash 를 키로 하는 Map 에 쿼리를 담습니다. 자세한 동작은 17주차 Fetch 및 요청 관리에 있습니다.
6.2 Set 으로 중복 제거와 선택 상태 관리
무한 스크롤에서 중복 거르기. 다음 페이지를 받는 사이에 새 매물이 등록되면 목록이 한 칸씩 밀려서, 이미 받은 매물이 다음 페이지에 또 옵니다.
export const mergeItems = (prev: Item[], next: Item[]) => {
const seenIds = new Set(prev.map((item) => item.id));
return [...prev, ...next.filter((item) => !seenIds.has(item.id))];
};
중복을 거르지 않으면 같은 매물이 두 번 보이고, React 가 같은 key 가 두 개라는 경고를 냅니다.
prev.some(...) 으로 하나씩 비교하면 새로 받은 매물마다 기존 목록을 한 바퀴 돕니다. 목록이 길어질수록 느려지는 무한 스크롤에서는 Set 이 맞습니다.
체크박스 선택 상태 관리하기. 목록에서 여러 개를 골라 한 번에 삭제하는 화면입니다.
interface ItemTableProps {
items: Item[];
}
export default function ItemTable({ items }: ItemTableProps) {
const [selectedIds, setSelectedIds] = useState<Set<number>>(() => new Set());
const toggle = (id: number) => {
setSelectedIds((prev) => {
const next = new Set(prev); // 복사본을 고쳐야 React 가 바뀐 걸 앎
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
const isAllSelected = items.length > 0 && selectedIds.size === items.length;
const toggleAll = () => {
setSelectedIds(
isAllSelected ? new Set() : new Set(items.map((item) => item.id)),
);
};
return (
<table>
<thead>
<tr>
<th>
<input
type="checkbox"
checked={isAllSelected}
onChange={toggleAll}
/>
</th>
<th>매물</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.id}>
<td>
<input
type="checkbox"
checked={selectedIds.has(item.id)}
onChange={() => toggle(item.id)}
/>
</td>
<td>{item.name}</td>
</tr>
))}
</tbody>
</table>
);
}
배열로 관리하면 행마다 selectedIds.includes(item.id) 를 부르고, 해제할 때마다 filter 로 새 배열을 만듭니다. Set 은 has, add, delete 가 모두 평균 O(1) 이고 같은 id 가 두 번 들어갈 일도 없습니다.
한 가지 주의할 점이 있습니다. 기존 Set 을 직접 고치면 화면이 바뀌지 않습니다.
setSelectedIds((prev) => {
prev.add(id);
return prev; // 같은 Set 이라 React 는 바뀐 게 없다고 보고 다시 그리지 않음
});
React 는 이전 상태와 새 상태를 참조로 비교합니다. 그래서 위 toggle 처럼 new Set(prev) 로 복사한 뒤 고쳐서 돌려줘야 합니다.
7. 정리 및 Q&A
7.1 Hash, Map, Set 의 차이
| Hash (해시 테이블) | Map | Set | |
|---|---|---|---|
| 정체 | 키를 칸 번호로 바꿔 저장하는 원리 | 키와 값을 쌍으로 저장 | 중복 없는 값의 모음 |
| 찾기 | 평균 O(1) | get(key) 평균 O(1) | has(value) 평균 O(1) |
| 쓰는 곳 | Map, Set 의 내부 구현 | 조회, 캐싱, 개수 세기 | 중복 제거, 선택 상태, 포함 여부 |
7.2 어떤 상황에서 무엇을 쓸까
| 이런 상황이면 | 쓰는 것 | 이유 |
|---|---|---|
순서가 중요하고 map, filter, 정렬을 쓰는 목록 | Array | 인덱스와 배열 메서드 |
| 필드가 정해진 데이터, API 요청·응답 | Object | JSON 변환과 타입 정의가 쉬움 |
| 키로 자주 찾고, 키를 계속 넣고 뺌 | Map | 평균 O(1) 조회, 추가·삭제에 최적화 |
| 키가 숫자·객체이거나 넣은 순서가 중요함 | Map | 키 타입과 순서를 그대로 유지 |
| 있는지 없는지만 중요하거나 중복을 없애야 함 | Set | 평균 O(1) has, 자동 중복 제거 |
데이터가 수십 개라면 배열 includes 로 충분합니다. 반복문 안에서 찾기를 반복하거나 데이터가 수천 개를 넘을 때 Map, Set 으로 바꾸면 됩니다.
7.3 복습 질문
① 해시 테이블 조회는 항상 O(1) 인가요?
아니요. 평균 O(1) 입니다. 여러 키가 같은 칸에 몰리면(충돌) 그 칸 안에서 키를 하나씩 비교해야 해서 최악에는 O(n) 입니다.
② 모양이 같은 객체를 새로 만들어 map.get 에 넘기면 왜 undefined 가 나오나요?
Map 은 객체 키를 참조로 비교합니다. 두 { id: 7 } 은 모양만 같은 서로 다른 객체라서 다른 키입니다. 서버 데이터는 id 같은 원시값을 키로 씁니다.
③ 객체 대신 Map 을 쓰면 좋은 경우는?
실행 중에 키를 계속 넣고 빼는 경우, 키가 숫자나 객체인 경우, 넣은 순서가 중요한 경우입니다. 사용자 입력을 키로 쓸 때 constructor 같은 물려받은 키와 부딪히지 않는 것도 장점입니다.
④ filter 안에서 includes 를 부르는 코드의 시간복잡도와 개선 방법은?
filter 한 바퀴마다 includes 가 한 바퀴를 돌아서 O(n × m) 입니다. favoriteIds 를 Set 으로 한 번 바꾸고 has 로 확인하면 O(n + m) 이 됩니다.
⑤ Set 상태에 prev.add(id) 를 하고 prev 를 반환하면 왜 화면이 안 바뀌나요?
React 는 새 상태가 이전 상태와 같은 참조면 바뀐 게 없다고 보고 다시 그리지 않습니다. new Set(prev) 로 복사한 뒤 고쳐서 반환해야 합니다.