본문으로 건너뛰기

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 인 사용자를 찾는다고 해 보겠습니다.

왼쪽은 배열 users 에서 find 로 id 7 을 찾는 모습으로, 0번 칸부터 하나씩 확인해 id 3, 12, 5 는 다르고 4번째 칸에서 찾는다. 사용자가 n 명이면 최대 n 번 확인한다. 오른쪽은 객체 usersById 에서 키 7 로 해당 칸에 바로 가서 1번 만에 찾는다

배열은 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]
1010번1번
1,0001,000번1번
1,000,0001,000,000번1번

배열은 찾는 사용자가 맨 끝에 있거나 아예 없으면 n 번을 다 확인합니다. 데이터에 비례해서 늘어나니 O(n) 입니다.

객체는 사용자가 100만 명이어도 한 번에 꺼냅니다. 데이터 개수와 상관없으니 O(1) 입니다.

자주 보는 세 가지를 그래프로 그리면 이렇습니다.

가로축은 데이터 개수 n, 세로축은 확인 횟수다. O(1) 은 데이터가 늘어도 바닥에 평평하게 머물고, O(n) 은 데이터에 비례해 대각선으로 올라가며, O(n²) 은 금방 가파르게 치솟아 그래프 위로 빠져나간다

표기읽는 법이름뜻예시
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개짜리 테이블에 넣으면 이렇게 됩니다.

키 audi, ford, kia, bmw 를 해시 함수 hash(key) % 8 에 넣으면 칸 번호 3, 3, 5, 6 이 나오고, 칸 8개짜리 해시 테이블의 해당 칸에 키와 값을 저장한다. audi 와 ford 는 칸 번호가 같아 3번 칸 안에 줄 세워 저장된다

저장할 때는 키를 해시 함수에 넣어 칸 번호를 얻고, 그 칸에 값을 넣습니다.

찾을 때도 같은 키를 넣으면 같은 칸 번호가 나오니까 그 칸만 보면 됩니다. 테이블에 키가 몇 개 있든 계산 한 번, 칸 하나라서 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]

나머지 차이는 표로 모았습니다.

ObjectMap
키 타입문자열, Symbol (숫자는 문자열로 바뀜)모든 값 (객체, 함수 포함)
기본 키프로토타입에서 물려받은 키 있음없음
순서정수 키가 먼저 오름차순, 나머지는 넣은 순서넣은 순서
크기Object.keys(obj).lengthmap.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 와 차이점​

ArraySet
중복허용허용 안 함
순서있음. 인덱스로 접근 (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"]

원본 배열 brands 는 bmw, benz, bmw, kia, benz 이고 세 번째 bmw 와 다섯 번째 benz 가 중복이다. new Set(brands) 로 Set 을 만들면 bmw, benz, kia 세 개만 처음 들어온 순서대로 남고, [...set] 으로 다시 배열 unique 를 만든다

배열을 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;
};

getItems(&#39;bmw&#39;) 를 부르면 cache.get(&#39;bmw&#39;) 로 저장된 값이 있는지 본다. 있으면 저장해 둔 Promise 를 바로 반환하고 요청은 나가지 않는다. 없으면 API 요청을 보내고 그 Promise 를 cache.set 으로 저장한 뒤 반환한다. 실패하면 cache.delete 로 지워 다음 호출 때 다시 요청한다

응답 대신 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 (해시 테이블)MapSet
정체키를 칸 번호로 바꿔 저장하는 원리키와 값을 쌍으로 저장중복 없는 값의 모음
찾기평균 O(1)get(key) 평균 O(1)has(value) 평균 O(1)
쓰는 곳Map, Set 의 내부 구현조회, 캐싱, 개수 세기중복 제거, 선택 상태, 포함 여부

7.2 어떤 상황에서 무엇을 쓸까​

이런 상황이면쓰는 것이유
순서가 중요하고 map, filter, 정렬을 쓰는 목록Array인덱스와 배열 메서드
필드가 정해진 데이터, API 요청·응답ObjectJSON 변환과 타입 정의가 쉬움
키로 자주 찾고, 키를 계속 넣고 뺌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) 로 복사한 뒤 고쳐서 반환해야 합니다.

참고 자료​