✨ 08. 모던 JS 문법 — 구조분해·스프레드·옵셔널 체이닝을 시니어처럼 쓰는 법

2026년 3월 8일 수정됨

📋 개요

구조분해 할당, 스프레드/나머지 연산자, 옵셔널 체이닝, 널 병합 연산자 — 매일 쓰는 ES6+ 문법의 깊은 이해와 실무 활용.

🎯 이 섹션을 읽고 나면:

  • 구조분해 할당의 모든 패턴(기본값, 별칭, 중첩, 함수 매개변수)을 자유롭게 쓴다.
  • 스프레드와 나머지 연산자를 상황에 맞게 구분해서 사용한다.
  • 옵셔널 체이닝(?.)과 널 병합(??)으로 방어 코드를 간결하게 작성한다.

📋 목차


📌 이 문서를 읽기 전에

⏱️ 예상 읽기 시간: 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 || defaultvalue ?? default
nulldefaultdefault
undefineddefaultdefault
0default ❌0 ✅
""default ❌"" ✅
falsedefault ❌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: 1undefined이므로 기본값 1 사용 (null이면 기본값 사용 안 됨! 구조분해 기본값은 undefined에만 적용)
  • team: "frontend" — 속성 자체가 없으므로 기본값 사용
  • 📌 핵심 기억법: "구조분해 기본값은 undefined일 때만 적용된다. nullnull이다."

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는 매일 쓰는 거니까 좀 자신 있다. 잘 자자.


🔗 더 알아보기