✨ 08. 모던 JS 문법 — 구조분해·스프레드·옵셔널 체이닝을 시니어처럼 쓰는 법
📋 개요
구조분해 할당, 스프레드/나머지 연산자, 옵셔널 체이닝, 널 병합 연산자 — 매일 쓰는 ES6+ 문법의 깊은 이해와 실무 활용.
🎯 이 섹션을 읽고 나면:
- 구조분해 할당의 모든 패턴(기본값, 별칭, 중첩, 함수 매개변수)을 자유롭게 쓴다.
- 스프레드와 나머지 연산자를 상황에 맞게 구분해서 사용한다.
- 옵셔널 체이닝(
?.)과 널 병합(??)으로 방어 코드를 간결하게 작성한다.
📋 목차
- 📌 이 문서를 읽기 전에
- 🤔 1. 왜 알아야 하는가?
- 📦 2. 구조분해 할당 (Destructuring)
- 📡 3. 스프레드 & 나머지 연산자
- 🛡️ 4. 옵셔널 체이닝 & 널 병합
- 💡 5. 실무 패턴 조합
- 📝 마무리 퀴즈
- 🐣 영철이의 퇴근 일기
- 🔗 더 알아보기
📌 이 문서를 읽기 전에
⏱️ 예상 읽기 시간: 18분(전체) / 핵심 파트만: 10분
🗺️ 이 문서의 흐름
[구조분해 할당] → [스프레드/나머지] → [옵셔널 체이닝 + 널 병합] → [실무 패턴 조합]
🎯 이 문서를 다 읽으면 할 수 있는 것
- API 응답 객체에서 필요한 값만 깔끔하게 뽑아쓰는 코드를 작성한다.
- 불변성을 유지하면서 객체/배열을 업데이트하는 스프레드 패턴을 사용한다.
-
Cannot read property of undefined에러를 옵셔널 체이닝으로 방어한다.
🗺️ 이 문서의 배경 세계관: '영수네 커뮤니티'
🐣 영철: "영호 님, API 응답 처리하는 코드가 너무 길어요.
response.data.user.profile.avatarUrl이런 식으로 매번 depth가 깊어서, 중간에null이면 에러가 나서if (response && response.data && response.data.user ...)이런 방어 코드를 잔뜩 썼는데... 5줄짜리 방어 코드가 더 길어요."
🦁 영호: "그 5줄짜리 방어 코드가 옵셔널 체이닝 하나로 줄어들어.
response?.data?.user?.profile?.avatarUrl— 끝. 그리고 그 API 응답에서 값 꺼내는 것도 구조분해 할당으로 한 줄에 정리할 수 있어. 오늘 모던 JS 문법 배우면 코드가 반으로 줄 거야."
🤔 1. 왜 알아야 하는가?
영철이가 오프닝에서 고민하던 그 5줄짜리 방어 코드가 어떻게 마법처럼 줄어드는지 확인해 보세요. 영호 리드 님이 "가독성과 안전성을 동시에 잡는 가장 쉬운 방법"이라고 극찬한 모던 문법의 힘입니다.
// ❌ 구식 코드 — 읽기 피로가 쌓이는 방식
const postId = apiResponse.data.post.id;
const postTitle = apiResponse.data.post.title;
const authorName = apiResponse.data.post.author.name;
const authorAvatar = apiResponse.data.post.author.avatarUrl;
// 이 코드가 왜 위험한가?
// apiResponse.data.post가 null/undefined면 → TypeError
// 방어 코드: if (apiResponse && apiResponse.data && ...) → 4줄 추가
// ✅ 모던 JS — 같은 결과, 1/3 코드량
const {
data: {
post: {
id: postId,
title: postTitle,
author: { name: authorName, avatarUrl: authorAvatar } = {},
} = {},
} = {},
} = apiResponse ?? {};모던 JS 문법을 잘 쓰면 가독성, 안전성, 코드량 세 가지를 동시에 잡는다.
📦 2. 구조분해 할당 (Destructuring)
배열 구조분해
배열의 원소들을 하나씩 꺼내 변수에 담는 정석적인 방법입니다. 순서가 중요한 배열의 특성을 이용해 필요한 값만 쏙쏙 골라낼 수 있습니다.
// 기본 사용
const [first, second, third] = [10, 20, 30];
console.log(first, second, third); // 10 20 30
// 건너뛰기
const [, , thirdPost] = ["React 가이드", "Next.js 가이드", "JS 가이드"];
console.log(thirdPost); // "JS 가이드"
// 기본값 (undefined일 때 사용)
const [a = 1, b = 2] = [10];
console.log(a, b); // 10 2 (b는 undefined → 기본값 2)
// 나머지 원소
const [head, ...tail] = [1, 2, 3, 4, 5];
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]
// 값 교환 (임시 변수 불필요)
let x = 1, y = 2;
[x, y] = [y, x];
console.log(x, y); // 2 1
// React useState와 완벽하게 맞는 이유
const [count, setCount] = useState(0); // 배열 구조분해!객체 구조분해
가장 활용도가 높은 패턴입니다. 객체의 키 이름을 그대로 사용하거나, 나만의 별칭을 붙여서 꺼내올 수도 있습니다.
// 영수네 커뮤니티 — API 응답 처리
const post = {
id: 42,
title: "클로저 완전정복",
author: "영철",
createdAt: "2026-03-05",
viewCount: 1234,
};
// 기본 사용
const { id, title, author } = post;
// 별칭 (변수 이름 바꾸기)
const { id: postId, title: postTitle } = post;
// postId = 42, postTitle = "클로저 완전정복"
// 기존 id, title 변수와 이름 충돌 방지
// 기본값
const { viewCount = 0, likeCount = 0 } = post;
// viewCount = 1234 (있으므로 기본값 사용 안 함)
// likeCount = 0 (없으므로 기본값 사용)
// 별칭 + 기본값 동시 사용
const { createdAt: publishedDate = "날짜 없음" } = post;
// publishedDate = "2026-03-05"함수 매개변수 구조분해
이것이 가장 실무에서 자주 쓰이는 패턴이다. 영호 리드 님이 퀴즈 Q3의 createPost 사례처럼 매개변수가 많아지면 바로 이 패턴을 쓰라고 권장한다 — "이름으로 넘기면 순서 실수가 없어요."
// ❌ 순진한 코드 — 매개변수 순서 외워야 함
function renderPost(postId, title, author, viewCount, likeCount) {
// 5번째 인자가 뭐였지?...
}
renderPost(42, "클로저", "영철", 1234, 56); // 순서 실수하기 쉬움
// ✅ 객체 구조분해 매개변수 — 이름으로 전달, 순서 무관
function renderPost({ id, title, author, viewCount = 0, likeCount = 0 }) {
return `[${id}] ${title} by ${author} (조회 ${viewCount}, 좋아요 ${likeCount})`;
}
renderPost({
id: 42,
title: "클로저 완전정복",
author: "영철",
viewCount: 1234,
// likeCount 생략 → 기본값 0
});
// React 컴포넌트가 이 패턴을 쓰는 이유
function PostCard({ title, author, viewCount = 0 }) {
return <div>{title} — {author} ({viewCount}회)</div>;
}중첩 구조분해
복잡하게 얽힌 API 응답 결과도 구조분해를 중첩해서 사용하면 단 한 줄로 깊숙한 곳의 데이터까지 도달할 수 있습니다.
// API 응답에서 깊이 중첩된 데이터 추출
const apiResponse = {
status: 200,
data: {
post: {
id: 42,
title: "클로저 완전정복",
author: {
name: "영철",
profile: {
avatarUrl: "https://cdn.youngsu.com/avatar/42.png",
},
},
tags: ["javascript", "closure"],
},
},
};
// 중첩 구조분해
const {
data: {
post: {
id,
title,
author: {
name: authorName,
profile: { avatarUrl },
},
tags: [firstTag], // 배열 구조분해도 중첩 가능
},
},
} = apiResponse;
console.log(id, title, authorName, avatarUrl, firstTag);
// 42, "클로저 완전정복", "영철", "https://...", "javascript"📡 3. 스프레드 & 나머지 연산자
같은 ... 기호지만, 위치에 따라 역할이 다르다.
스프레드 (Spread)
"펼치기"라는 이름처럼, 배열이나 객체의 내용물을 낱개로 흩뿌려주는 역할을 합니다. 기존 데이터를 훼손하지 않고 새로운 복사본을 만들 때 필수적입니다.
// 배열 스프레드
const frontendPosts = ["React", "Next.js", "CSS"];
const backendPosts = ["Node.js", "Express", "DB"];
// ❌ concat으로 합치기
const allPosts = frontendPosts.concat(backendPosts);
// ✅ 스프레드로 합치기 (더 직관적)
const allPosts = [...frontendPosts, ...backendPosts];
// ["React", "Next.js", "CSS", "Node.js", "Express", "DB"]
// 배열 복사 (얕은 복사)
const copied = [...frontendPosts];
copied.push("TypeScript");
console.log(frontendPosts); // ["React", "Next.js", "CSS"] — 원본 불변
// 객체 스프레드 — 불변 업데이트 (React 상태 업데이트의 핵심!)
const user = { name: "영철", role: "junior", level: 1 };
// 특정 필드만 업데이트 (불변)
const updatedUser = { ...user, level: 2 }; // level만 변경, 나머지는 복사
// { name: "영철", role: "junior", level: 2 }
// 나중에 오는 값이 덮어씀
const merged = { ...user, role: "senior", level: 5 };
// role: "senior", level: 5 로 덮어씌워짐
// 함수 호출에서 배열을 개별 인자로
const nums = [3, 1, 4, 1, 5, 9];
Math.max(...nums); // Math.max(3, 1, 4, 1, 5, 9) = 9나머지 (Rest)
스프레드와 모양은 같지만 역할은 정반대입니다. 흩어져 있는 원소들을 하나의 덩어리로 묶어줄 때 사용합니다.
// 함수 나머지 매개변수
function logPostActivity(postId, ...events) {
// events는 배열로 수집
console.log(`게시글 ${postId} 활동: ${events.join(", ")}`);
}
logPostActivity(42, "조회", "좋아요", "댓글", "공유");
// "게시글 42 활동: 조회, 좋아요, 댓글, 공유"
// 구조분해에서 나머지
const { name, role, ...rest } = { name: "영철", role: "junior", level: 1, team: "frontend" };
console.log(name, role); // "영철" "junior"
console.log(rest); // { level: 1, team: "frontend" }
// 배열 구조분해에서 나머지
const [first, second, ...remaining] = [1, 2, 3, 4, 5];
console.log(first, second); // 1 2
console.log(remaining); // [3, 4, 5]🛡️ 4. 옵셔널 체이닝 & 널 병합
옵셔널 체이닝 (?.)
영호 리드 님이 영철이의 코드를 단 한 줄로 줄여준 비결입니다. "값이 없을지도 모른다"는 불안감을 코드로 우아하게 표현하는 방법입니다.
// ❌ 순진한 방어 코드 — 5줄짜리 if 체인
function getAvatarUrl(user) {
if (user && user.profile && user.profile.avatar) {
return user.profile.avatar.url;
}
return null;
}
// ✅ 옵셔널 체이닝 — 1줄
function getAvatarUrl(user) {
return user?.profile?.avatar?.url;
}
// user, profile, avatar 중 어느 하나라도 null/undefined면 → undefined 반환 (에러 없음)
// 메서드 호출에도 사용
const result = someObject?.methodThatMayNotExist?.();
// 배열 접근에도 사용
const firstPost = posts?.[0]?.title;
// 영수네 커뮤니티 실무 예시
async function renderUserProfile(userId) {
const user = await fetchUser(userId);
const avatarUrl = user?.profile?.avatarUrl ?? "/default-avatar.png";
const displayName = user?.nickname ?? user?.name ?? "익명";
const followerCount = user?.stats?.followerCount ?? 0;
render({ avatarUrl, displayName, followerCount });
}널 병합 연산자 (??)
null이나 undefined만 콕 집어서 기본값으로 대체하고 싶을 때 사용합니다. 기존의 || 연산자가 0이나 빈 문자열까지 걸러버리던 부작용을 해결해 줍니다.
// ??는 null/undefined일 때만 오른쪽 값 사용 (0, false, ""는 유효한 값으로 취급!)
const viewCount = 0;
// ❌ OR 연산자 — 0, false, ""도 falsy로 처리
const display1 = viewCount || "조회수 없음";
console.log(display1); // "조회수 없음" ← 0은 유효한 조회수인데!
// ✅ 널 병합 — null/undefined만 처리
const display2 = viewCount ?? "조회수 없음";
console.log(display2); // 0 ✅ (0은 유효한 값이므로 그대로 사용)
// 실무 예시 — API 응답 기본값 처리
const { title = "제목 없음", likeCount = 0 } = post;
// ↑ 구조분해 기본값도 null/undefined에만 적용됨 (?? 와 동일)
// ??= 연산자 (ES2021)
let cachedData = null;
cachedData ??= await fetchData(); // null/undefined일 때만 할당|| vs ?? 구분이 중요한 이유:
| 값 | value || default | value ?? default |
|---|---|---|
null | default | default |
undefined | default | default |
0 | default ❌ | 0 ✅ |
"" | default ❌ | "" ✅ |
false | default ❌ | false ✅ |
💡 5. 실무 패턴 조합
지금까지 배운 모든 문법을 영수네 커뮤니티의 실제 대시보드 로직에 녹여내면 어떤 모습이 될까요? 가독성과 안전성이 비약적으로 향상된 코드를 감상해 보세요.
// 영수네 커뮤니티 — API 응답 처리 종합 패턴
async function loadUserDashboard(userId) {
const response = await fetchUserData(userId);
// 1. 옵셔널 체이닝 + 널 병합으로 안전하게 추출
const {
user: {
name = "익명",
email,
profile: { avatarUrl = "/default.png", bio = "" } = {},
stats: { postCount = 0, followerCount = 0, followingCount = 0 } = {},
} = {},
} = response?.data ?? {};
// 2. 스프레드로 상태 불변 업데이트
const currentState = getState();
setState({
...currentState,
user: { name, email, avatarUrl, bio },
stats: { postCount, followerCount, followingCount },
lastUpdated: new Date().toISOString(),
});
}
// React 컴포넌트에서의 전형적인 패턴
function PostEditor({ post, onSave, onCancel }) {
// 구조분해 + 기본값
const {
id,
title = "",
content = "",
tags = [],
isPublished = false,
} = post ?? {};
// 스프레드로 불변 업데이트
const handleTagAdd = (newTag) => {
onSave({ ...post, tags: [...tags, newTag] });
};
const handlePublish = () => {
onSave({ ...post, isPublished: true, publishedAt: new Date().toISOString() });
};
}📝 마무리 퀴즈
Q1. 아래 코드의 출력 결과를 예측하라.
const user = { name: "영철", role: "junior", level: undefined };
const { name, role = "member", level = 1, team = "frontend" } = user;
console.log(name, role, level, team);✅ 정답: "영철", "junior", 1, "frontend"
💡 상세 해설:
name:"영철"— 직접 있음role:"junior"—"junior"가 있으므로 기본값"member"사용 안 함level:1—undefined이므로 기본값1사용 (null이면 기본값 사용 안 됨! 구조분해 기본값은undefined에만 적용)team:"frontend"— 속성 자체가 없으므로 기본값 사용- 📌 핵심 기억법: "구조분해 기본값은
undefined일 때만 적용된다.null은null이다."
Q2. ||와 ??의 차이를 코드로 보여주고, 어느 상황에서 각각 써야 하는지 설명하라.
✅ 정답: ||는 falsy 값 전체를 대체, ??는 null/undefined만 대체.
💡 상세 해설:
const score = 0;
const name = "";
// || — falsy를 모두 대체 → 의도치 않은 동작
console.log(score || "점수 없음"); // "점수 없음" ← 0도 falsy!
console.log(name || "이름 없음"); // "이름 없음" ← ""도 falsy!
// ?? — null/undefined만 대체
console.log(score ?? "점수 없음"); // 0 ✅
console.log(name ?? "이름 없음"); // "" ✅||를 써야 할 때: 0, 빈 문자열, false가 의미 없는 값으로 취급될 때??를 써야 할 때: 0, 빈 문자열, false도 유효한 값이고, 정말 null/undefined만 걸러내고 싶을 때- 📌 핵심 기억법: "조회수는 0도 유효한 값이다.
count || "없음"이면 조회수 0인 글이 '없음'으로 표시된다. 이럴 때??를 써라."
Q3. 영철이의 테스트 타임 — 아키텍처 설계
영호 리드가 코드 리뷰에서 이렇게 코멘트를 달았다: "영철 님, 이 함수 매개변수가 8개인데, 절반은 옵션이에요. 순서를 외워야 하고, 새 옵션을 추가할 때마다 기존 호출부를 전부 수정해야 해요. 어떻게 리팩토링하면 좋을까요?"
// 리팩토링 전
function createPost(title, content, authorId, tags, isPublished, scheduledAt, coverImageUrl, metaDescription) { ... }
createPost("제목", "내용", 42, ["js"], false, null, null, null);✅ 정답: 옵션 매개변수들을 하나의 객체로 묶고, 구조분해 + 기본값을 사용한다.
💡 상세 해설:
// ✅ 리팩토링 후 — 옵션 객체 패턴
function createPost(title, content, authorId, options = {}) {
const {
tags = [],
isPublished = false,
scheduledAt = null,
coverImageUrl = null,
metaDescription = "",
} = options;
// ...
}
// 훨씬 명확한 호출
createPost("제목", "내용", 42, {
tags: ["js", "closure"],
isPublished: true,
// 나머지는 기본값 사용
});
// 새 옵션 추가 시 기존 호출부 수정 불필요- 📌 핵심 기억법: "매개변수가 3개 이상이고 일부가 옵션이라면, 옵션 객체 패턴을 고려해라. 이름으로 전달하면 순서 실수도 없다."
🐣 영철이의 퇴근 일기
오늘은 영어 문법 배우는 것처럼 "아, 이런 표현이 있었구나!" 하는 느낌이었다. 사실 구조분해는 쓰고 있었는데, 중첩 구조분해나 별칭+기본값 조합은 몰랐다. 이제 API 응답 처리할 때 저 패턴 써먹어봐야지.
옵셔널 체이닝은 정말... 왜 진작에 배웠을까 싶다. && 체인으로 방어 코드 쌓던 게 너무 구식이었다.
그리고 ||와 ?? 차이 — 진짜 중요하다. 조회수가 0일 때 "없음"으로 나오는 버그, 그거 내가 실제로 만든 거다. 그때 왜 그런지 몰랐는데 오늘 이해했다.
💡 오늘의 교훈: "모던 자바스크립트 문법은 단순히 코드를 줄이는 것이 아니라, 개발자의 '의도'를 더 명확히 드러내는 도구입니다. 구조분해는 '이 값들이 필요해',
??는 '진짜 값이 없을 때만 이걸 써',?.는 '이 경로가 안전하지 않을 수 있어'라는 메시지를 담고 있습니다."
오늘 저녁은 편의점 김밥으로 때웠다. 주말엔 제대로 밥 해먹어야지. 배열 고차함수 챕터가 남았는데, map/filter/reduce는 매일 쓰는 거니까 좀 자신 있다. 잘 자자.