falsy 값을 처리하는 방법
`TypeError: Cannot read properties of null (reading 'name')` 오류 발생
- 이 오류는 null인 값에서 `name` 속성을 읽으려고 해서 발생한다.
- 예를 들어, 아래 코드에서 menu가 null이면 에러가 발생함
console.log(menu.name); // menu가 null이면 오류 발생
해결 방법 4가지
✅ 1. `?.`(옵셔널 체이닝) 사용 (가장 추천!)
console.log(menu?.name); // menu가 null이면 undefined 반환 (오류 없음)
✔ `?.`(옵셔널 체이닝)을 사용하면 menu가 null이어도 오류 없이 undefined를 반환
✔ Vue 템플릿에서도 사용 가능
<p>{{ menu?.name }}</p> <!-- menu가 null이면 undefined로 처리됨 -->
✅ 2. `||`(논리 OR) 연산자로 기본값 설정
console.log(menu?.name || "기본값"); // menu가 null이면 "기본값" 출력
✔ null일 때 "기본값"을 대신 출력하도록 설정하면 안전함.
✅ 3. `??`(Nullish coalescing) 연산자로 기본값 설정
console.log(menu?.name ?? "기본값");
✔ `??`는 null 또는 undefined일 때만 "기본값"을 반환
✔ `||`와 다르게 0이나 ''(빈 문자열)는 유지됨
✅ 4. if 문으로 null 체크
if (menu) {
console.log(menu.name);
} else {
console.log("menu가 존재하지 않습니다.");
}
✔ 기본적인 if 문으로 null을 체크하고 안전하게 처리 가능
상황에 따른 연산자 사용
1. 옵셔널 체이닝 (`?.`) → "안전하게 접근할 때"
📌 객체의 속성이 null일 수도 있을 때 사용
✅ 언제 쓰나?
- 객체가 null일 수도 있는 상황에서 안전하게 속성을 접근할 때
- Vue 템플릿에서 안전하게 바인딩할 때
- 중첩된 객체의 속성에 접근할 때
✅ 예제
const user = null;
console.log(user?.name); // ❌ 오류 없이 undefined 반환
console.log(user?.profile?.age); // ❌ 중첩된 속성도 안전하게 처리
👉 user가 null이지만 `?.` 덕분에 undefined가 반환되어 오류가 나지 않음.
✅ Vue 템플릿에서 사용 예시
<p>{{ user?.name }}</p> <!-- user가 null이어도 에러 안 남 -->
2. Null 병합 연산자 (`??`) → "null 또는 undefined일 때 기본값"
📌 값이 null 또는 undefined일 때만 기본값을 설정하고 싶을 때 사용
✅ 언제 쓰나?
- null 또는 undefined일 때만 기본값을 제공하고 싶을 때
- 0, ''(빈 문자열), false 같은 값은 유효하게 인정해야 할 때
✅ 예제
const value = null;
console.log(value ?? "기본값"); // "기본값" 출력 (value가 null이므로)
const count = 0;
console.log(count ?? 10); // 0 (기본값이 적용되지 않음)
👉 `??`는 null과 undefined만 기본값으로 대체하기 때문에, 0이나 ""(빈 문자열) 같은 값은 유지됨.
3. 논리 OR 연산자 (`||`) → "Falsy 값일 때 기본값"
📌 값이 false, 0, ""(빈 문자열), null, undefined이면 기본값을 설정할 때 사용
✅ 언제 쓰나?
- "값이 없을 때" 기본값을 주고 싶을 때
- null, undefined, 0, "", false를 전부 "없음"으로 간주할 때
✅ 예제
const name = "";
console.log(name || "기본값"); // "기본값" 출력 (빈 문자열은 false로 간주됨)
const count = 0;
console.log(count || 100); // 100 출력 (0은 false로 간주됨)
👉 `||`는 false, 0, "" 같은 값도 전부 기본값으로 대체해 버림.
📌 정리
✔ 객체 속성 접근할 때 `?.`
✔ null/undefined에만 기본값 적용할 때 `??`
✔ 빈 값(0, "", false)까지 기본값 적용할 때 `||`
각 메뉴의 옵션 리스트가 열리지 않는 에러 수정
🔥 다른 곳을 클릭했을 때 `openedMenuIndex`가 항상 null로 초기화되는 현상 발생
문제 원인
현재 코드에서 `toggleMenu`함수를 실행하면 `openedMenuIndex`와 `openedMenuName`을 설정하는데, 이후 곧바로 `closeMenuOnClickOutside`함수가 실행되면서 값이 다시 null로 초기화되어 메뉴가 화면상에 뜨지 않을 수 있다.
즉, 코드의 실행 순서에 따라 메뉴를 열자마자 닫혀버리는 문제가 발생하는 것이다.
발생 과정
- 사용자가 특정 메뉴의 `⋮` 버튼을 클릭하여 `toggleMenu(menu.name, index)`가 실행됨.
- `openedMenuIndex`와 `openedMenuName`이 변경됨.
- 클릭 이벤트가 document에 전달되면서 곧바로 `closeMenuOnClickOutside`가 실행됨.
- `closeMenuOnClickOutside`는 `openedMenuIndex`가 가리키는 `menuRefs` 요소에 `event.target`이 포함되지 않으면 `openedMenuIndex = null`로 설정.
- 결과적으로, 메뉴가 열리자마자 닫히기 때문에 화면상에 옵션 리스트가 아예 나타나지 않는 것처럼 보임.
해결 방법: `setTimeout`을 사용하여 비동기 처리
이 문제는 `toggleMenu`가 실행된 후, `closeMenuOnClickOutside`가 너무 빨리 실행되면서 발생하는 것이므로 약간의 딜레이를 주어` toggleMenu`가 먼저 정상적으로 실행되도록 만들면 해결할 수 있다는 판단
const toggleMenu = (name, index) => {
setTimeout(() => {
openedMenuName.value = name;
openedMenuIndex.value = index;
}, 0);
};
왜 `setTimeout(() => {...}, 0)`인가?
- `setTimeout`을 0ms로 설정하면 브라우저의 이벤트 루프에서 다음 이벤트 사이클에서 실행되도록 예약됨
- 즉, setTimeout 안에 있는 코드는 이벤트 큐에 등록되고, 현재 실행 중인 모든 동기 코드가 완료된 다음 틱(next tick) 에 실행된다.
- 이렇게 하면 `toggleMenu`의 코드가 완전히 실행된 후 `closeMenuOnClickOutside`가 실행되므로, `openedMenuIndex`가 의도대로 설정된 상태를 유지할 수 있다.
- 버튼 클릭
- `toggleMenu` 실행 → `setTimeout`으로 실행 예약
- 클릭 이벤트 전파 → `closeMenuOnClickOutside` 실행 → 메뉴 닫힘
- 현재 이벤트 루프 끝남
- 이제 `setTimeout` 예약된 코드 실행 → `openedMenuIndex = index` 실행됨
- 닫힌 메뉴가 다시 열리는 게 아니라, "닫히기 전에 메뉴를 연다"가 되는 것처럼 동작
- 즉, 닫힘보다 나중에 실행되게끔 미루는 트릭이라고 볼 수 있다. 이러한 과정을 통해`setTimeout(..., 0)`으로 딜레이 없이 비동기 예약하면, 클릭 이벤트의 전파가 끝난 다음에 `openedMenuIndex`를 설정할 수 있고, 의도대로 메뉴를 열거나 상태를 바꿀 수 있게 되는 것이다.
👨💻 [프로그래머스] 완주하지 못한 선수
function solution(participant, completion) {
var answer = '';
let dic = {};
answer = participant.reduce((dic, num, i) => {
dic[num] ? dic[num]++ : dic[num] = 1;
return dic;
}, {});
answer = completion.reduce((dic, num, i) => {
dic[num] ? dic[num]-- : dic[num];
if(!dic[num]) delete dic[num];
return dic;
}, answer);
return Object.keys(answer)[0];
}
- 최대 선수 10만 (입력개수)
- `reduce`는 전체 배열을 순회하므로 `O(N)`의 시간복잡도를 가진다. `O(N)`은 1000만개의 입력을 커버할 수 있다.
- 따라서 `reduce`를 두번 돌리더라도 시간 안에 해결할 수 있는 문제라 생각해서 위와 같이 문제를 풀었다.
역시 다른 풀이를 살펴보니 훨씬 직관적인 풀이방법이 많았다😂
다른 풀이
function solution(participant, completion) {
const map = new Map();
for(let i = 0; i < participant.length; i++) {
let a = participant[i],
b = completion[i];
map.set(a, (map.get(a) || 0) + 1);
map.set(b, (map.get(b) || 0) - 1);
}
for(let [k, v] of map) {
if(v > 0) return k;
}
return 'nothing';
}
🔍 코드 분석
1. Map을 이용한 참가자 및 완주자 카운팅
const map = new Map();
- Map 객체를 생성하여, 참가자와 완주자의 수를 기록하는 역할
2. 참가자와 완주자의 등장 횟수 기록
for(let i = 0; i < participant.length; i++) {
let a = participant[i],
b = completion[i];
map.set(a, (map.get(a) || 0) + 1);
map.set(b, (map.get(b) || 0) - 1);
}
- participant와 completion 배열을 같은 인덱스에서 하나씩 가져옴
- map.set(a, (map.get(a) || 0) + 1);
- 참가자를 map에 추가
- map.get(a) || 0 → 기존 값이 없으면 0으로 초기화 후 +1
- 즉, 참가자의 등장 횟수를 기록한다.
- map.set(b, (map.get(b) || 0) - 1);
- completion에 있는 이름의 등장 횟수를 -1 한다.
- 완주자가 있으면 해당 참가자의 등장 횟수가 줄어든다.
3. 완주하지 못한 사람 찾기
for(let [k, v] of map) {
if(v > 0) return k;
}
- map을 순회하면서, 값(v)가 0보다 큰 사람을 찾는다.
- 0보다 크다는 것은 참가했지만 완주하지 못했다는 뜻이므로, 그 이름(k)을 반환
4. 예외 처리
return 'nothing';
- 만약 모든 사람이 완주했으면 "nothing"을 반환한다.
- 그러나 문제에서 항상 한 명이 완주하지 못하는 조건을 제시했기 때문에, 실제 실행될 일은 거의 없음
🛠 예제 테스트
console.log(solution(["leo", "kiki", "eden"], ["eden", "kiki"]));
// 결과: "leo"
console.log(solution(["maria", "tom", "jack", "maria"], ["jack", "maria", "tom"]));
// 결과: "maria"
🚀 시간 복잡도 분석
- for 루프가 participant.length번 실행 → O(N)
- for 루프에서 Map을 조회하고 수정하는 연산은 평균적으로 O(1)
- 최종적으로 O(N) + O(N) = O(N)
✅ 효율적인 풀이: O(N) 시간 복잡도로 해결할 수 있다.
(sort를 사용하는 풀이보다 더 빠름 → 정렬은 O(N log N)이므로)
🔍 for...in vs for...of 차이점 정리
구분 | for...in | for...of |
사용 대상 | 객체(Object), 배열(Array) | 배열(Array), 이터러블(Iterable) |
반환값 | 키(인덱스 또는 프로퍼티명) | 값 (배열 요소, 이터러블 값) |
용도 | 객체 속성 순회, 배열 인덱스 순회 | 배열 요소 순회, Set, Map 등 이터러블 순회 |
성능 | 비교적 느림 (객체 속성 조회 포함) | 빠름 (직접 값에 접근) |
주의할 점 | 배열 순회 시 원치 않는 속성도 포함될 수 있음 | Object에는 사용 불가능 |
1️⃣ for...in → 객체와 배열의 "키"를 순회
const obj = { a: 1, b: 2, c: 3 };
for (let key in obj) {
console.log(key, obj[key]);
}
// 출력:
// a 1
// b 2
// c 3
- for...in은 객체의 속성을 반복할 때 사용
- 배열에도 사용 가능하지만, 인덱스(키)를 반환하므로 비추천
🔹 배열에서 사용 예시 (비추천)
const arr = ["apple", "banana", "cherry"];
for (let i in arr) {
console.log(i, arr[i]);
}
// 출력:
// 0 apple
// 1 banana
// 2 cherry
- i는 인덱스(0, 1, 2)를 반환
⚠ 주의점: for...in은 배열에 사용하면 비효율적이며, 추가된 속성도 포함될 수 있음
Array.prototype.customProp = "test";
for (let i in arr) {
console.log(i); // 0, 1, 2, customProp (예상치 못한 값 포함)
}
2️⃣ for...of → 배열이나 이터러블의 "값"을 순회
const arr = ["apple", "banana", "cherry"];
for (let value of arr) {
console.log(value);
}
// 출력:
// apple
// banana
// cherry
- for...of는 배열 요소를 직접 가져옴 (값 반환)
- Set, Map, String 등 이터러블(iterable) 객체에도 사용 가능
🔹 객체(Object)에는 사용 불가!
const obj = { a: 1, b: 2 };
for (let value of obj) { // ❌ TypeError: obj is not iterable
console.log(value);
}
🔹 Set과 Map에서 사용
const set = new Set(["apple", "banana", "cherry"]);
for (let value of set) {
console.log(value);
}
// apple
// banana
// cherry
const map = new Map([["a", 1], ["b", 2]]);
for (let [key, value] of map) {
console.log(key, value);
}
// a 1
// b 2
✅ 정리
- 객체(Object) 순회 → for...in (키 반환)
- 배열(Array) 순회 → for...of (값 반환) ✅ 추천!
- 이터러블(Set, Map 등) 순회 → for...of 사용 가능
- 배열에서 for...in 사용 비추천 (불필요한 속성 포함 가능)
➡ 배열에는 for...of, 객체에는 for...in을 사용하자!
👨💻 [프로그래머스] 실패율
function solution(N, stages) {
var answer = [];
for(let i = 1; i <= N; i++){
//시도한 사람
const tried = stages.filter((it)=> it >= i).length;
//실패한 사람
const failed = stages.filter((it)=> it === i).length;
answer.push([i, (failed/tried)]);
}
answer.sort((a, b) => b[1] - a[1])
return answer.map((it) => it[0]);
}
내일 다시 보기... 답안 보고 풀었음