문서 읽는 데 54분 · C4

C-4: 배열과 객체

전체 23강 중 15강 · HTML·CSS·JS
난이도 · 입문

안녕하세요, 홍순구 튜터입니다. 지난 시간 우리는 함수의 새로운 차원을 열었어요. 변수가 보이는 범위인 스코프, 함수가 자기 변수를 기억하는 클로저, 함수를 값처럼 넘기는 콜백까지요. 마지막엔 이 셋을 모아 게시물을 카테고리별로 걸러내는 filterPostsfor 루프로 한 땀 한 땀 직접 만들었죠.

그런데 마무리에서 살짝 약속한 게 있어요. JavaScript 배열에는 그 "걸러내기"를 한 줄로 해주는 도구가 이미 들어 있다고요. 오늘은 바로 그 도구들을 만납니다. 직접 만든 forEachItemfilterPosts가, 오늘은 단 한 줄로 줄어드는 모습을 보게 될 거예요.

오늘 배울 건 크게 두 덩어리예요. 하나는 배열을 다루는 도구들 — 요소를 넣고 빼는 push·pop·splice·slice, 그리고 배열을 돌며 변환하고 걸러내고 합산하는 forEach·map·filter·reduce예요. 다른 하나는 객체 — 지난 시간 살짝 맛만 본 { caption, category }를 제대로 파헤치고, 거기서 값을 한 번에 꺼내는 구조 분해, 배열·객체를 펼쳐 합치는 **스프레드(...)**까지 배워요.

   지난 시간 (C-3)                      오늘 (C-4)
   ┌────────────────────────┐         ┌────────────────────────┐
   │ for 루프로 직접 순회     │   ──▶   │ map / filter 한 줄로     │
   │ forEachItem(...)        │         │ posts.filter(...)       │
   │ { caption, category }   │         │ 객체 · 구조분해 · 스프레드 │
   │  (맛보기)               │         │  (정식)                 │
   └────────────────────────┘         └────────────────────────┘

오늘 우리가 만든 코드는 같은 일을 하면서도 훨씬 짧고 읽기 좋아질 거예요. 마지막엔 피드 데이터를 걸러내고 화면용 카드로 바꾸는 진짜 인스타그램스러운 흐름을 완성합니다.

💡 오늘 수업의 핵심 — "배열에는 돌고·바꾸고·거르고·합치는 도구가 이미 들어 있다. 객체는 관련된 값을 한 다발로 묶고, 구조 분해와 스프레드로 자유롭게 꺼내고 합친다." 🎯

🎯 학습 목표

  • 배열 메서드 push·pop·splice·slice로 배열에 요소를 넣고 빼고 잘라냅니다.
  • forEachmap의 차이를 이해하고, 지난 시간 만든 forEachItem을 내장 메서드로 대체합니다.
  • filter로 조건에 맞는 요소만 걸러내고, reduce로 배열을 하나의 값으로 합산합니다.
  • 객체를 만들고, 점 표기법으로 값을 읽고 바꾸고 추가합니다.
  • 구조 분해 할당으로 객체·배열에서 필요한 값을 한 번에 꺼냅니다.
  • **스프레드(...)**로 배열·객체를 펼쳐 합치고, 원본을 지키며 복사합니다.
  • filter·map·구조 분해를 모아 피드 데이터를 카드로 렌더링합니다.

Step 1: 배열 메서드 — push·pop·splice·slice

지금까지 배열을 여러 번 썼어요. 지난 시간 tags = ["#여행", "#맛집", "#일상"] 같은 배열을 만들어 for 루프로 돌았죠. 그런데 배열에 요소를 새로 넣거나 빼려면 어떻게 할까요? 배열에는 이런 일을 해주는 메서드(method)가 붙어 있어요. 메서드는 "어떤 값에 점(.)을 찍어 부르는 함수"예요. tag.length에서 .length로 길이를 꺼냈던 것처럼, 배열.push(...)처럼 점을 찍어 부르죠.

가장 기본인 네 가지부터 봐요. 스토리에 올라온 친구 목록을 배열로 두고 만져볼게요.

// instagram-clone-frontend/js/main.js
const stories = ["jaehoon", "minji", "seungwoo"];

stories.push("yuna");           // 맨 뒤에 추가 (원본을 바꿈)
console.log(stories);           // ["jaehoon", "minji", "seungwoo", "yuna"]

const popped = stories.pop();   // 맨 뒤 하나를 빼서 돌려줌 (원본을 바꿈)
console.log(popped);            // "yuna"
console.log(stories);           // ["jaehoon", "minji", "seungwoo"]

push는 배열 맨 뒤에 요소를 추가해요. pop은 반대로 맨 뒤 요소를 하나 빼서 돌려줘요. 둘 다 원본 배열을 직접 바꾼다는 게 중요해요. push("yuna") 뒤엔 stories가 네 개가 됐고, pop() 뒤엔 다시 세 개로 줄었죠. 그리고 pop은 빼낸 값("yuna")을 돌려주니까, 변수에 받아 쓸 수도 있어요.

   push("yuna")  →  맨 뒤에 끼움
   [ jaehoon ][ minji ][ seungwoo ]          ← 끼우기
                                  ▲
                              [ yuna ]

   pop()  →  맨 뒤 하나 빼서 돌려줌
   [ jaehoon ][ minji ][ seungwoo ][ yuna ]
                                       │ 빼서 반환
                                       ▼  "yuna"

splice와 slice — 비슷한 이름, 다른 일

이번엔 배열 중간을 건드리는 splice와, 배열의 일부를 복사하는 slice예요. 이름이 비슷해서 헷갈리기 쉬운데, 하는 일은 완전히 달라요.

// splice(시작, 삭제개수, 넣을값) — 중간을 잘라내거나 끼워넣기 (원본을 바꿈)
stories.splice(1, 1, "dohyun"); // index 1 부터 1개 빼고 그 자리에 "dohyun"
console.log(stories);           // ["jaehoon", "dohyun", "seungwoo"]

// slice(시작, 끝) — 원본은 그대로 두고 잘라낸 복사본을 돌려줌
const firstTwo = stories.slice(0, 2);  // index 0 ~ 1
console.log(firstTwo);          // ["jaehoon", "dohyun"]
console.log(stories);           // 원본은 그대로 (slice 는 안 건드림)

splice(1, 1, "dohyun")은 "index 1부터 1개를 빼고, 그 자리에 "dohyun"을 넣어라"는 뜻이에요. 그래서 "minji"가 빠지고 "dohyun"이 들어왔죠. splice는 원본을 직접 바꿔요.

반면 slice(0, 2)는 "index 0부터 2 직전(0과 1)까지 잘라서 복사본을 돌려줘라"예요. 끝 번호 2는 포함 안 돼요. 중요한 건 slice는 원본을 안 건드린다는 거예요. 마지막 줄에서 stories를 다시 찍어보면 그대로죠.

💡 배열 메서드는 두 부류로 나뉘어요. push·pop·splice처럼 원본을 바꾸는 것과, slice처럼 원본은 두고 새 걸 돌려주는 것. 글자 한 끗 차이(splice ↔ slice)지만 동작은 정반대예요. "이 메서드가 원본을 바꾸나, 새 걸 주나"를 늘 의식하면 헷갈릴 일이 줄어요. 다음 Step부터 만날 map·filter는 모두 원본을 안 건드리는 쪽이에요.


Step 2: forEach와 map — "각각"을 다루는 두 가지 방법

지난 시간 우리는 배열을 돌며 각 요소마다 콜백을 부르는 forEachItem을 직접 만들었어요. for 루프를 감싸서요. 그런데 이런 순회는 워낙 자주 쓰니까, JavaScript 배열에 이미 만들어져 있어요. 바로 forEach예요.

// instagram-clone-frontend/js/main.js
const captions = ["제주 여행", "오늘의 맛집", "평범한 일상"];

// forEach: 각 요소마다 콜백 실행 (결과를 모으지 않음 — 출력 같은 "부작용"용)
captions.forEach((text) => {
  console.log("게시물: " + text);
});

콘솔 출력은 이래요.

게시물: 제주 여행
게시물: 오늘의 맛집
게시물: 평범한 일상

captions.forEach(콜백)은 배열의 요소를 하나씩 꺼내 콜백에 넘겨요. 우리가 만든 forEachItem(captions, 콜백)과 하는 일이 똑같죠. 다만 직접 for 루프를 짤 필요 없이, 배열에 점만 찍어 부르면 돼요. 지난 시간 콜백의 원리를 직접 만들어봤으니, 이 내장 메서드도 "아, 안에서 콜백을 부르는 거구나" 하고 바로 이해될 거예요.

map — 각 요소를 "변환"해서 새 배열로

forEach는 각 요소로 무언가를 할 뿐, 결과를 따로 모으진 않아요. 그런데 종종 "각 요소를 바꿔서 새 배열을 만들고 싶을" 때가 있어요. 글자 수만 모은 배열, 이모지를 붙인 배열처럼요. 이때 쓰는 게 map이에요.

// map: 각 요소를 "변환"해서 새 배열로 모음 (원본은 그대로)
const lengths = captions.map((text) => text.length);
console.log(lengths);    // [5, 6, 6]  ← 각 글자 수로 변환된 새 배열

const labels = captions.map((text) => "📷 " + text);
console.log(labels);     // ["📷 제주 여행", "📷 오늘의 맛집", "📷 평범한 일상"]
console.log(captions);   // 원본은 그대로 (map 은 새 배열만 만들어요)

map은 콜백이 돌려준 값을 모아 새 배열을 만들어요. (text) => text.length는 각 글자 수를 돌려주니까 [5, 6, 6]이 되고, (text) => "📷 " + text는 이모지 붙인 글자를 돌려주니까 그런 배열이 되죠. 원본 captions는 그대로예요.

   captions:  [ "제주 여행" ][ "오늘의 맛집" ][ "평범한 일상" ]
                   │              │              │
                map(text => text.length)  ← 각각을 변환
                   ▼              ▼              ▼
   lengths:   [     5      ][      6      ][      6      ]   ← 새 배열

핵심 차이는 이거예요. forEach는 각 요소로 무언가 하고 끝(출력 같은 부작용), map은 각 요소를 변환해 새 배열을 돌려줘요. "결과 배열이 필요하면 map, 그냥 각각 처리만 하면 forEach"로 기억하면 편해요.

🙋 학생 질문 — "튜터님, map 도 그냥 console.log 만 하면 안 되나요?"

해도 동작은 해요. 하지만 그러면 map의 의미가 사라져요. map"변환한 새 배열"을 돌려주는 게 핵심인데, 콜백에서 아무것도 안 돌려주고 console.log만 하면, 돌아오는 배열이 [undefined, undefined, undefined]가 돼버려요.

const wrong = captions.map((text) => {
  console.log(text);   // 출력만 하고 아무것도 return 안 함
});
console.log(wrong);    // [undefined, undefined, undefined]

출력만 하고 싶으면 forEach를, 변환한 배열이 필요하면 map을 쓰세요. "결과를 모을 거냐 아니냐"가 둘을 가르는 기준이에요.


Step 3: filter와 reduce — 걸러내고 합산하기

이제 지난 시간 약속을 지킬 차례예요. filterPostsfor 루프로 직접 만들었던 그 "걸러내기"가, filter 메서드 한 줄로 줄어들어요.

filter는 배열을 돌면서 콜백이 참(true)을 돌려준 요소만 모아 새 배열을 만들어요. 좋아요 수가 담긴 배열에서 인기 게시물(100 이상)만 골라볼게요.

// instagram-clone-frontend/js/main.js
const likeCounts = [120, 8, 340, 56, 1200];

// filter: 조건이 참인 요소만 모아 새 배열로
const popular = likeCounts.filter((n) => n >= 100);
console.log(popular);    // [120, 340, 1200]

콜백 (n) => n >= 100은 각 숫자가 100 이상인지 참/거짓을 돌려줘요. filter는 참이 나온 요소(120, 340, 1200)만 모아 새 배열로 돌려주죠. 8과 56은 조건이 거짓이라 빠져요.

지난 시간 filterPosts에서 우리가 직접 짠 게 바로 이거예요. let matched = 0을 두고, for 루프를 돌며, if로 조건을 확인하고, 맞으면 모으고... 그 여러 줄이 filter 한 줄에 다 들어 있어요.

   for 루프로 직접 (지난 시간)        filter 한 줄로 (오늘)
   ┌──────────────────────────┐
   │ let result = [];          │
   │ for (let i = 0; ...) {    │     likeCounts.filter(
   │   if (조건) {              │  ─▶   (n) => n >= 100
   │     result.push(items[i]);│     )
   │   }                       │
   │ }                         │
   └──────────────────────────┘

reduce — 배열을 하나의 값으로 접기

mapfilter는 배열을 돌려주지만, 가끔은 배열 전체를 하나의 값으로 모으고 싶어요. 좋아요 총합, 최댓값, 평균처럼요. 이때 쓰는 게 reduce예요. 이름 그대로 배열을 한 값으로 "줄여(reduce)" 나가요.

// reduce: 배열을 하나의 값으로 "접어" 나가기 (합계·최댓값 등)
// (누적값 sum, 현재값 n) => 다음 누적값,  0 은 시작값
const likeSum = likeCounts.reduce((sum, n) => sum + n, 0);
console.log(likeSum);    // 1724  (0 에서 시작해 하나씩 더함)

reduce의 콜백은 인자가 두 개예요. **지금까지 모은 누적값(sum)현재 요소(n)**죠. 콜백이 돌려준 값이 다음 차례의 sum이 돼요. 맨 뒤 0sum의 시작값이고요. 그래서 0 + 120 = 120, 120 + 8 = 128, ... 이렇게 하나씩 더해 나가 최종 1724가 나와요.

   시작값 0
     │  + 120  → 120
     │  + 8    → 128
     │  + 340  → 468
     │  + 56   → 524
     │  + 1200 → 1724   ← 최종 결과

처음엔 인자 두 개가 헷갈릴 수 있어요. 괜찮아요. "sum은 지금까지 쌓인 값, n은 지금 더할 값"만 기억하면 돼요. 합계 말고도 "전부 곱하기", "가장 큰 값 찾기" 같은 데도 같은 모양으로 쓸 수 있어요.


Step 4: 객체 리터럴과 점 표기법 — 관련 데이터를 한 다발로

지난 시간 filterPosts에서 게시물을 { caption: "...", category: "..." } 모양으로 잠깐 봤죠. "이렇게 생겼다"만 맛봤는데, 이제 제대로 배워요. 이게 바로 객체(object)예요.

배열은 값을 순서대로 늘어놓죠. ["jaehoon", "minji"]처럼 0번, 1번 순서로 담기고요. 그런데 게시물 하나에는 글 내용, 카테고리, 좋아요 수처럼 서로 다른 종류의 정보가 붙어 있어요. 이걸 순서로 외우긴 어렵죠. "0번이 글, 1번이 카테고리..." 이러면 헷갈려요. 그래서 이름표(key)를 붙여 값을 묶는 게 객체예요.

// instagram-clone-frontend/js/main.js
const post = {
  id: 1,
  caption: "제주 여행 다녀왔어요",
  category: "travel",
  likeCount: 120
};

중괄호 { } 안에 이름표: 값을 쉼표로 나열했어요. id, caption, category, likeCount라는 이름표(key)에 각각 값이 짝지어 있죠. 이렇게 관련 정보를 한 덩어리로 묶으면, 게시물 하나를 변수 하나로 깔끔하게 들고 다닐 수 있어요.

   배열 (순서로 접근)            객체 (이름표로 접근)
   [ "제주 여행" , "travel" ]    { caption: "제주 여행",
       0번        1번             category: "travel",
                                  likeCount: 120 }
   posts[0], posts[1]           post.caption, post.category
   (몇 번인지 외워야 함)         (이름으로 바로 꺼냄)

점 표기법으로 값 읽고·바꾸고·추가하기

객체 안의 값을 꺼낼 땐 **점(.)**을 써요. post.caption이라고 하면 caption 이름표의 값이 나오죠. 값을 바꾸거나 새로 추가하는 것도 점으로 해요.

// 점(.) 표기법으로 값을 읽고 바꿔요
console.log(post.caption);    // "제주 여행 다녀왔어요"
console.log(post.likeCount);  // 120
post.likeCount = 121;         // 좋아요 +1 (속성 값 변경)
console.log(post.likeCount);  // 121

// 없던 속성도 점 표기법으로 새로 추가돼요
post.isLiked = true;
console.log(post.isLiked);    // true

post.caption으로 글 내용을 읽고, post.likeCount = 121로 좋아요를 하나 올렸어요. 좋아요를 눌렀다는 표시(isLiked)는 원래 객체에 없던 이름표인데, post.isLiked = true라고 적으면 새 속성으로 추가돼요. 객체는 이렇게 자유롭게 값을 읽고, 바꾸고, 늘릴 수 있어요.

중괄호 단축 문법

객체를 만들 때, 넣으려는 값이 이미 같은 이름의 변수에 들어 있으면 한 번만 적어도 돼요.

// 중괄호 단축 문법: 변수 이름과 key 가 같으면 한 번만 써요 (ES6)
const author = "minji";
const postCount = 150;
const summary = { author, postCount };  // { author: "minji", postCount: 150 }
console.log(summary);

원래대로면 { author: author, postCount: postCount }라고 적어야 하는데, 이름표와 변수 이름이 똑같으니 { author, postCount }로 줄였어요. 결과는 { author: "minji", postCount: 150 }로 똑같죠. 자주 쓰는 편한 문법이라 알아두면 좋아요.

💡 지난 시간 filterPosts에서 본 { caption, category }가 바로 이 객체였어요. 그때는 "이렇게 생겼다"만 봤는데, 이제 직접 만들고 점으로 값을 꺼낼 수 있게 됐죠. 객체는 앞으로 정말 자주 만나요. 게시물도, 사용자도, 댓글도 전부 객체로 표현하거든요.


Step 5: 구조 분해 할당 — 한 번에 여러 값 꺼내기

객체에서 값을 꺼낼 때마다 post.caption, post.category처럼 매번 점을 찍는 게 번거로울 때가 있어요. 특히 여러 값을 한꺼번에 꺼낼 때요. 이때 구조 분해 할당(destructuring)을 쓰면 필요한 값을 한 번에 변수로 빼낼 수 있어요.

// instagram-clone-frontend/js/main.js
const account = { handle: "hong_tutor", fans: 1240, isPublic: true };
const { handle, fans } = account;   // 속성 이름과 똑같은 변수로 받아요
console.log(handle, fans);          // hong_tutor 1240

const { handle, fans } = account는 "account에서 handlefans 속성을 꺼내 같은 이름의 변수에 담아라"예요. 한 줄로 변수 두 개가 만들어졌죠. const handle = account.handle; const fans = account.fans;를 한 번에 적은 셈이에요.

   account = { handle: "hong_tutor", fans: 1240, isPublic: true }
                  │                   │
   const { handle, fans } = account   ← 이름표로 콕 집어 꺼냄
           │       │
        "hong_tutor"  1240

기본값 — 없는 속성은 기본값으로

꺼내려는 속성이 객체에 없을 수도 있어요. 그럴 때 =기본값을 정해두면, 속성이 없을 때 그 값으로 채워져요.

// 기본값: 객체에 없는 속성은 기본값으로 채워요
const { bio = "소개가 없어요" } = account;
console.log(bio);                   // 소개가 없어요

account에는 bio 속성이 없어요. 그래서 bio는 기본값 "소개가 없어요"가 됐죠. 지난 시간 함수 매개변수 기본값(greet(name = "게스트"))과 똑같은 발상이에요. "값이 없으면 이걸 써"라는 거죠.

배열도 분해할 수 있어요

객체는 이름표로 꺼냈지만, 배열은 순서대로 꺼내요. 위치가 곧 이름인 셈이죠.

// 배열 분해: 위치(순서)대로 받아요
const topThree = ["jaehoon", "minji", "seungwoo"];
const [first, second] = topThree;
console.log(first, second);         // jaehoon minji

// 나머지(...rest): 앞을 빼고 남은 걸 배열로 묶어요
const [winner, ...runnersUp] = topThree;
console.log(winner);                // jaehoon
console.log(runnersUp);             // ["minji", "seungwoo"]

const [first, second] = topThree는 배열의 0번을 first에, 1번을 second에 담아요. 객체는 { }로, 배열은 [ ]로 분해한다는 것만 다르고 발상은 같죠.

그리고 ...rest(나머지)를 쓰면, 앞의 몇 개를 빼고 남은 전부를 배열로 묶어요. [winner, ...runnersUp]은 첫 요소를 winner에, 나머지를 runnersUp 배열에 담죠. 지난 시간 함수의 rest parameter(...postIds)에서 본 점 세 개가 여기서도 "나머지 묶기"로 쓰여요.

함수가 배열을 돌려주면 그 자리에서 분해

구조 분해가 빛나는 순간이 하나 있어요. 함수가 배열을 돌려줄 때예요. 지난 시간 배운 클로저를 응용해, 좋아요 수를 함수 안에 숨기고 [읽기 함수, 누르기 함수] 두 개를 배열로 돌려줘볼게요.

// 함수가 배열을 돌려주면, 그 자리에서 분해로 받을 수 있어요.
// C-3 클로저를 응용 — 좋아요 수를 함수 안에 숨기고 [읽기, 누르기]를 돌려줘요.
function makeLikeBox() {
  let count = 0;
  const read = () => count;
  const press = () => {
    count = count + 1;
    return count;
  };
  return [read, press];             // 배열에 두 함수를 담아 돌려줌
}
const [readLikes, pressLike] = makeLikeBox();  // 배열 분해로 한 번에 받기
pressLike();
pressLike();
console.log(readLikes());           // 2  (숨겨진 count 가 기억돼요)

makeLikeBoxcount를 함수 안에 숨겨두고(클로저), 그 count를 읽는 read와 1 올리는 press 두 함수를 배열에 담아 돌려줘요. 받는 쪽은 const [readLikes, pressLike] = makeLikeBox()로 그 배열을 바로 분해하죠. pressLike()를 두 번 부른 뒤 readLikes()를 부르면 2가 나와요. 숨겨진 count가 클로저로 계속 기억되고 있으니까요.

지난 시간 배운 클로저(상태 숨기기)와 오늘 배운 배열 분해가 한 줄에 자연스럽게 만나는 모습이에요.


Step 6: 스프레드 연산자(...) — 펼쳐서 합치기

점 세 개(...)를 또 만나요. 이번엔 "나머지 묶기"가 아니라 반대로 펼치기예요. 배열이나 객체 앞에 ...를 붙이면, 그 안의 요소들이 낱낱이 풀려 나와요. 이걸 스프레드(spread, 펼치기)라고 해요.

// instagram-clone-frontend/js/main.js
const morning = ["jaehoon", "minji"];
const evening = ["seungwoo", "yuna"];
const allStories = [...morning, ...evening];  // 두 배열을 펼쳐 합치기
console.log(allStories);            // ["jaehoon", "minji", "seungwoo", "yuna"]

// 맨 앞에 새 요소를 끼우며 복사 (원본 morning 은 그대로)
const withNewFirst = ["dohyun", ...morning];
console.log(withNewFirst);          // ["dohyun", "jaehoon", "minji"]

[...morning, ...evening]은 두 배열을 펼쳐서 하나로 합쳐요. morning의 요소들과 evening의 요소들이 풀려 나와 새 배열에 나란히 담기죠. ["dohyun", ...morning]처럼 새 요소와 섞어서 끼울 수도 있어요. 중요한 건 원본(morning)은 그대로 두고 새 배열을 만든다는 거예요.

   morning: [ jaehoon ][ minji ]      evening: [ seungwoo ][ yuna ]
                 └──┐  └──┐               ┌──┘       ┌──┘
                    ▼     ▼               ▼          ▼
   allStories: [ jaehoon ][ minji ][ seungwoo ][ yuna ]   ← 새 배열

객체도 펼칠 수 있어요

스프레드는 객체에도 통해요. 객체를 펼쳐 복사하면서, 일부 속성만 바꿔 새 객체를 만들 수 있죠.

// 객체도 펼칠 수 있어요 — 복사하면서 일부 속성만 덮어쓰기
const basePost = { id: 1, caption: "제주 여행", likeCount: 120 };
const likedPost = { ...basePost, likeCount: 121, isLiked: true };
console.log(likedPost);             // likeCount 만 121 로 바뀐 새 객체
console.log(basePost.likeCount);    // 120  ← 원본은 그대로 (불변성)

{ ...basePost, likeCount: 121, isLiked: true }basePost의 모든 속성을 펼쳐 담은 뒤, likeCount는 121로 덮어쓰고 isLiked를 새로 추가해요. 같은 이름표가 겹치면 뒤에 적은 값이 이겨요. 그래서 likeCount가 121이 되죠.

여기서 꼭 짚을 게 있어요. 새 객체(likedPost)를 만들었지만 원본(basePost)은 그대로예요. 마지막 줄에서 basePost.likeCount를 찍으면 여전히 120이죠. 이렇게 "원본을 건드리지 않고 바뀐 새 걸 만드는" 성질을 불변성(immutability)이라고 해요.

💡 Step 4에서 post.likeCount = 121은 원본을 직접 바꿨어요. 반면 스프레드는 원본을 두고 새 객체를 만들죠. 둘 다 쓸모가 있지만, 데이터를 다룰 땐 "원본을 지키며 새 걸 만드는" 방식이 안전할 때가 많아요. 어디서 값이 바뀌었는지 추적하기 쉽거든요. 지난 시간 클로저로 상태를 보호한 것과 같은 결의 고민이에요.


Step 7: 실전 — 피드 데이터 렌더링 (filter + map + 구조분해)

자, 오늘 배운 걸 다 모아볼 시간이에요. 지난 시간 for 루프로 한 땀 한 땀 만든 filterPosts를, 오늘 배운 객체 배열과 배열 메서드로 다시 써봐요. 진짜 인스타그램이 피드를 그리는 흐름과 닮아 있어요.

먼저 게시물 다섯 개를 객체 배열로 준비해요. 지난 시간 posts와 닮았지만, 이번엔 idlikeCount까지 갖춘 정식 객체예요.

// instagram-clone-frontend/js/main.js
const feed = [
  { id: 1, caption: "제주 여행 다녀왔어요", category: "travel", likeCount: 120 },
  { id: 2, caption: "오늘의 맛집 발견", category: "food", likeCount: 8 },
  { id: 3, caption: "평범한 일상", category: "daily", likeCount: 56 },
  { id: 4, caption: "발리 서핑 도전", category: "travel", likeCount: 340 },
  { id: 5, caption: "집밥 한 끼", category: "food", likeCount: 1200 }
];

이제 "여행 게시물만 골라내기"를 filter 한 줄로 해요. 지난 시간 filterPosts의 그 여러 줄이, 오늘은 정말 한 줄이에요.

// 1) 여행 게시물만 걸러내기 (filterPosts 의 for 루프가 filter 한 줄로!)
const travelPosts = feed.filter((item) => item.category === "travel");
console.log("여행 게시물 " + travelPosts.length + "개");  // 여행 게시물 2개

feed.filter((item) => item.category === "travel")는 각 게시물의 category"travel"인지 확인해, 참인 것만 모아요. 다섯 중 두 개(제주 여행, 발리 서핑)가 골라지죠. travelPosts.length로 개수도 바로 알 수 있어요.

걸러낸 게시물을 카드 문자열로 — map + 구조 분해

골라낸 게시물을 화면에 그릴 카드 문자열로 바꿔볼게요. map으로 각 게시물을 문자열로 변환하는데, 콜백 매개변수에서 바로 구조 분해를 써서 필요한 값만 꺼내요.

// 2) 걸러낸 게시물을 화면에 그릴 카드 문자열로 변환 (map + 구조분해)
//    백틱(``) 문법은 다음 시간에 — 지금은 + 로 이어 붙여요.
const cards = travelPosts.map(({ caption, likeCount }) => {
  return caption + " — 좋아요 " + likeCount + "개";
});
console.log(cards);
// ["제주 여행 다녀왔어요 — 좋아요 120개", "발리 서핑 도전 — 좋아요 340개"]

map(({ caption, likeCount }) => ...)를 보세요. 콜백이 게시물 객체를 통째로 받는 대신, { caption, likeCount }필요한 속성만 바로 분해해서 받았어요. Step 5에서 배운 구조 분해를 콜백 매개변수에 그대로 쓴 거죠. 그리고 문자열은 지난 시간처럼 +로 이어 붙였어요. 더 간결한 문법은 다음 시간에 만나요.

filter와 map을 한 줄로 연결하기

filtermap도 배열을 돌려주니까, 점을 이어 붙여 연달아 쓸 수 있어요. 이걸 메서드 체인(chain, 사슬처럼 잇기)이라고 해요.

// 3) filter → map 을 한 번에 연결(chain)
const foodCards = feed
  .filter((item) => item.category === "food")
  .map((item) => item.caption + " (맛집)");
console.log(foodCards);  // ["오늘의 맛집 발견 (맛집)", "집밥 한 끼 (맛집)"]

feed.filter(...).map(...)은 먼저 맛집 게시물만 거른 뒤(filter), 그 결과를 바로 카드 문자열로 바꿔요(map). filter가 돌려준 배열에 곧장 .map을 이어 붙인 거죠. 중간 변수 없이 "거르고 → 바꾸고"가 한 흐름으로 이어져요.

   feed (5개)
     │  .filter(category === "food")   ← 맛집만 거르기
     ▼
   [ 오늘의 맛집, 집밥 ]  (2개)
     │  .map(caption + " (맛집)")       ← 카드로 바꾸기
     ▼
   ["오늘의 맛집 발견 (맛집)", "집밥 한 끼 (맛집)"]

지난 시간 filterPosts 한 함수에 스코프·클로저·콜백을 다 녹였듯, 오늘은 filter·map·구조 분해가 이 몇 줄에 다 모였어요. 진짜 인스타그램도 이렇게 데이터를 거르고 변환해서 화면에 뿌려요. 우리가 방금 그 핵심 흐름을 손으로 만들어본 거예요.


마무리

오늘 우리는 배열과 객체를 자유자재로 다루는 도구를 손에 넣었어요. 지난 시간 for 루프로 직접 짠 순회가, 오늘은 배열에 점만 찍으면 되는 메서드로 짧아졌죠. 되짚어볼게요.

  • 배열 메서드push·pop·splice는 원본을 바꾸고, slice는 복사본을 돌려줘요. "원본을 바꾸나, 새 걸 주나"를 늘 의식하세요.
  • forEach와 mapforEach는 각 요소로 무언가 하고 끝, map은 각 요소를 변환해 새 배열을 돌려줘요.
  • filter와 reducefilter는 조건에 맞는 요소만 걸러 배열로, reduce는 배열을 하나의 값으로 접어요.
  • 객체 — 관련된 값을 이름표(key)로 묶어요. 점(.)으로 읽고·바꾸고·추가하죠.
  • 구조 분해 — 객체는 { }로, 배열은 [ ]로 필요한 값을 한 번에 꺼내요. 기본값과 ...rest도 함께요.
  • 스프레드(...) — 배열·객체를 펼쳐 합치고, 원본을 지키며 새 걸 만들어요(불변성).

배열 메서드가 처음엔 많아 보일 수 있어요. 괜찮아요. "돌기는 forEach, 바꾸기는 map, 거르기는 filter, 합치기는 reduce" 이 네 동사만 손에 익히면, 나머지는 코드를 읽다가 자연스럽게 채워져요. 오늘 지난 시간 코드가 한 줄로 줄어드는 걸 직접 봤으니, 이 도구들이 왜 사랑받는지 느꼈을 거예요.

다음 시간 예고

오늘 카드 문자열을 만들 때 caption + " — 좋아요 " + likeCount + "개"처럼 +로 이어 붙였죠. 글자와 변수를 섞을수록 따옴표와 +가 많아져서 슬슬 번거로워요. 다음 시간엔 백틱(`)과 ${ }를 쓰는 템플릿 리터럴을 배워요. `좋아요 ${likeCount}개`처럼, 글자 안에 변수를 바로 끼워 넣는 훨씬 깔끔한 문법이죠. 오늘 만든 카드가 한결 읽기 좋아질 거예요.

그리고 지금까지 모든 함수와 데이터를 main.js 한 파일에 차곡차곡 쌓아왔는데, 파일이 점점 길어지고 있어요. 다음 시간엔 코드를 역할별로 여러 파일로 나누는 법(모듈)도 함께 배워요. 로그인은 로그인 파일에, 피드는 피드 파일에. 점점 진짜 프로젝트의 모양을 갖춰갈 거예요. 기대하세요!


과제

오늘 배운 배열 메서드와 객체를 직접 손에 익혀볼 차례예요. 기초 → 응용 → 탐구 순서로 풀어보세요. 모든 과제는 콘솔(console.log)에서 확인하고, 오늘 배운 내용만으로 충분히 풀 수 있어요. 백틱(`)과 ${ }는 다음 시간 거니까, 문자열은 +로 이어 붙이면 돼요.

[구현] map과 filter로 인기 게시물 골라내기 (기초)

오늘 만든 feed 배열을 그대로 써서, 좋아요가 100 이상인 게시물의 글 내용만 뽑아보세요.

  • feed.filter(...)likeCount가 100 이상인 게시물만 걸러내세요.
  • 걸러낸 결과에 .map(...)을 이어 붙여(또는 따로) 각 게시물의 caption만 모은 배열을 만드세요.
  • console.log로 그 배열을 찍어, 인기 게시물 세 개("제주 여행 다녀왔어요", "발리 서핑 도전", "집밥 한 끼")의 글만 나오는지 확인하세요.

[구현] reduce로 전체 좋아요 합산하기 (응용)

feed 배열의 모든 게시물 좋아요를 더해 총합을 구해보세요.

  • feed.reduce(...)를 써서 각 게시물의 likeCount를 모두 더하세요. 누적값과 현재 게시물을 받는 콜백을 쓰고, 시작값은 0으로 두세요.
  • 콜백 안에서는 sum + item.likeCount 모양으로 누적값에 현재 게시물의 좋아요를 더하면 돼요.
  • 결과가 1724(120+8+56+340+1200)가 나오는지 console.log로 확인하세요.

[탐구] 스프레드로 게시물에 좋아요 누르기 (탐구)

스프레드의 불변성을 직접 확인해보세요. 원본을 지키면서 좋아요만 올린 새 게시물을 만드는 거예요.

  • feed[0](첫 게시물)을 스프레드로 펼쳐, likeCount만 1 올리고 isLiked: true를 추가한 새 객체를 만드세요. const liked = { ...feed[0], likeCount: feed[0].likeCount + 1, isLiked: true };처럼 적으면 돼요.
  • 새 객체(liked)의 likeCountisLiked를 찍어 121, true가 나오는지 확인하세요.
  • 그다음 원본 feed[0].likeCount를 찍어보세요. 여전히 120인가요? 왜 원본이 안 바뀌었는지, "스프레드는 새 객체를 만든다"는 점과 연결해 한 문장으로 정리해보세요.

생각해볼 주제

정답을 적는 문제가 아니에요. 오늘 배운 것의 "왜"를 곱씹어보는 질문들이에요. 스스로 답을 만들어본 뒤, 예시답안과 비교해보세요.

1. for 루프가 멀쩡한데 왜 map·filter를 쓸까?

지난 시간 for 루프로도 게시물을 잘 걸러냈어요. 동작은 똑같은데, 오늘은 굳이 filter·map으로 바꿨죠. for 루프는 "변수 i를 0부터 시작해, 조건을 확인하고, 1씩 늘리고, 배열에서 꺼내고..."를 매번 직접 적어야 해요. 반면 filter는 "무엇을 거를지"만 적으면 되죠. 코드를 읽는 사람 입장에서, 둘 중 어느 쪽이 "이 코드가 무슨 일을 하는지" 한눈에 들어올까요? "어떻게 도는가"를 감추고 "무엇을 하는가"만 남기는 게 왜 읽기 좋은지 생각해보세요.

2. 객체는 배열과 무엇이 다르고, 언제 쓸까?

게시물을 ["제주 여행", "travel", 120] 배열로 둘 수도 있고, { caption: "제주 여행", category: "travel", likeCount: 120 } 객체로 둘 수도 있어요. 배열로 두면 "0번이 글, 1번이 카테고리, 2번이 좋아요"를 외워야 하고, 객체로 두면 이름표로 바로 꺼내죠. 어떤 데이터일 때 배열이 어울리고, 어떤 데이터일 때 객체가 어울릴까요? "순서가 중요한 데이터"와 "종류가 다른 값들의 묶음"이라는 관점에서 둘을 갈라보세요.

3. 원본을 바꾸는 것과 새로 만드는 것, 어느 쪽이 안전할까?

Step 4의 post.likeCount = 121은 원본을 직접 바꿨고, Step 6의 스프레드는 원본을 두고 새 객체를 만들었어요. 둘 다 좋아요를 올리는 결과는 같아요. 그런데 여러 코드가 같은 게시물 데이터를 함께 본다고 상상해보세요. 한 곳에서 원본을 직접 바꿔버리면, 다른 곳은 자기도 모르게 바뀐 데이터를 보게 되죠. 반대로 새 객체를 만들면 원본은 안전해요. "값이 언제 어디서 바뀌는지 추적하기"라는 관점에서, 원본을 지키는 게 왜 도움이 되는지 곱씹어보세요.

✅ 예시 답안정답 보기

과제와 생각해볼 주제의 예시답안이에요. 정답이 하나만 있는 건 아니에요. 변수 이름은 취향대로 골라도 좋아요. 중요한 건 배열 메서드로 돌고·바꾸고·거르고·합치고, 객체와 구조 분해·스프레드로 값을 자유롭게 다뤘는가 예요.


🎯 [과제 1 예시답안] map과 filter로 인기 게시물 골라내기

핵심 접근

오늘 만든 feed 배열을 그대로 쓰는 과제예요. 핵심은 두 메서드를 순서대로 잇는 거예요. 먼저 filter로 좋아요 100 이상인 게시물만 거르고, 그 결과에 map을 이어 붙여 caption만 뽑아요. "거르고(filter) → 바꾸기(map)"라는 흐름이 오늘 Step 7에서 본 체인과 똑같죠. filter가 게시물 객체 배열을 돌려주면, 거기에 곧장 .map을 붙여 글자 배열로 변환하면 돼요.

예시 구현

// instagram-clone-frontend/js/main.js 맨 아래에 이어서
// (feed 는 오늘 수업에서 이미 만들었어요)

const popularCaptions = feed
  .filter((item) => item.likeCount >= 100)
  .map((item) => item.caption);
console.log(popularCaptions);

feed.filter((item) => item.likeCount >= 100)은 좋아요가 100 이상인 게시물(제주 여행 120, 발리 서핑 340, 집밥 1200)만 골라요. 8과 56은 빠지죠. 그 결과 배열에 .map((item) => item.caption)을 이어 붙이니, 게시물 객체가 글 내용 문자열로 변환돼 글만 모인 배열이 나와요.

[ '제주 여행 다녀왔어요', '발리 서핑 도전', '집밥 한 끼' ]

채점 포인트

항목 확인 내용
filter 조건 likeCount >= 100으로 인기 게시물만 걸렀는가
map 변환 걸러낸 게시물에서 caption만 뽑아 새 배열로 만들었는가
결과 확인 인기 게시물 세 개의 글만 나오는지 console.log로 확인했는가

흔한 실수

  • filter와 map의 순서를 바꿈map으로 먼저 caption만 뽑으면, 그 뒤엔 likeCount가 사라져서 거를 수가 없어요. 거를 땐 객체가 통째로 필요하니까, filter를 먼저 하고 map을 나중에 해야 해요.
  • map 콜백에서 return을 빠뜨림 — 중괄호 { }를 쓴 콜백은 return을 적어야 값이 돌아와요. (item) => { item.caption }처럼 return을 빼면 [undefined, undefined, ...]가 나와요. 한 줄이면 (item) => item.caption처럼 중괄호와 return을 함께 생략하세요.

🎯 [과제 2 예시답안] reduce로 전체 좋아요 합산하기

핵심 접근

reduce로 배열을 하나의 값(총합)으로 접는 과제예요. 핵심은 콜백의 두 인자예요. 첫째는 지금까지 쌓인 누적값(sum), 둘째는 현재 게시물(item)이죠. 콜백이 sum + item.likeCount를 돌려주면, 그 값이 다음 차례의 sum이 돼요. 시작값 0에서 출발해 다섯 게시물의 좋아요를 하나씩 더해 나가면 총합이 나와요.

예시 구현

// instagram-clone-frontend/js/main.js 맨 아래에 이어서
// (feed 는 오늘 수업에서 이미 만들었어요)

const totalLikes = feed.reduce((sum, item) => sum + item.likeCount, 0);
console.log(totalLikes);  // 1724

reducesum을 0에서 시작해, 게시물을 하나씩 돌며 item.likeCount를 더해요. 0 + 120 = 120, 120 + 8 = 128, 128 + 56 = 184, 184 + 340 = 524, 524 + 1200 = 1724. 그래서 최종 총합 1724가 나오죠.

1724

채점 포인트

항목 확인 내용
두 인자 사용 콜백이 누적값(sum)과 현재 게시물(item)을 받는가
likeCount 누적 sum + item.likeCount로 좋아요를 더해 나갔는가
시작값 reduce의 두 번째 인자로 시작값 0을 넘겼는가

흔한 실수

  • 시작값 0을 빠뜨림feed.reduce((sum, item) => sum + item.likeCount)처럼 시작값을 안 넘기면, 첫 번째 요소가 객체 통째로 sum에 들어가요. 그러면 객체 + 숫자가 돼서 "[object Object]120..." 같은 이상한 문자열이 나와요. 숫자를 더할 땐 시작값 0을 꼭 넘기세요.
  • item.likeCount 대신 item을 더함sum + item이라고 적으면 객체를 통째로 더하려 해서 결과가 깨져요. 게시물에서 좋아요 숫자를 꺼내려면 item.likeCount로 점을 찍어야 해요.

🎯 [과제 3 예시답안] 스프레드로 게시물에 좋아요 누르기

핵심 접근

스프레드의 불변성을 눈으로 확인하는 과제예요. 핵심은 feed[0]을 직접 바꾸는 게 아니라, 펼쳐서 새 객체를 만드는 거예요. { ...feed[0], likeCount: ..., isLiked: true }로 원본의 모든 속성을 펼쳐 담은 뒤, likeCount만 1 올리고 isLiked를 새로 추가해요. 같은 이름표는 뒤에 적은 값이 이기니까 likeCount가 새 값으로 덮어써지죠. 그러고 나서 원본을 찍어보면, 여전히 그대로인 걸 확인할 수 있어요.

예시 구현

// instagram-clone-frontend/js/main.js 맨 아래에 이어서
// (feed 는 오늘 수업에서 이미 만들었어요)

const liked = { ...feed[0], likeCount: feed[0].likeCount + 1, isLiked: true };
console.log(liked.likeCount);     // 121
console.log(liked.isLiked);       // true
console.log(feed[0].likeCount);   // 120  ← 원본은 그대로!

{ ...feed[0], ... }는 첫 게시물의 속성(id, caption, category, likeCount)을 전부 펼쳐 담아요. 그 뒤에 likeCount: feed[0].likeCount + 1로 좋아요를 121로 덮어쓰고, isLiked: true를 추가하죠. 새 객체 liked는 121에 좋아요 표시가 붙었지만, 원본 feed[0]은 손대지 않았으니 여전히 120이에요.

121
true
120

한 문장 정리 예시: "스프레드는 원본을 펼쳐 새 객체를 만들기 때문에, 새 객체의 likeCount를 121로 바꿔도 원본 feed[0]은 건드려지지 않아 그대로 120이 된다."

채점 포인트

항목 확인 내용
스프레드 복사 { ...feed[0], ... }로 원본을 펼쳐 새 객체를 만들었는가
속성 덮어쓰기·추가 likeCount를 1 올리고 isLiked: true를 추가했는가
불변성 확인 원본 feed[0].likeCount가 여전히 120인지 확인했는가

흔한 실수

  • 스프레드 없이 원본을 직접 바꿈feed[0].likeCount = 121로 바꾸면 좋아요는 올라가지만 원본이 바뀌어요. 이러면 "불변성"을 확인하는 과제의 의미가 사라지죠. 원본을 지키려면 반드시 { ...feed[0] }로 펼쳐 새 객체를 만들어야 해요.
  • 속성 순서를 거꾸로 적음{ likeCount: 121, ...feed[0] }처럼 스프레드를 뒤에 적으면, 펼쳐진 원본 likeCount(120)가 앞의 121을 덮어써서 다시 120이 돼요. 덮어쓸 값은 항상 스프레드 뒤에 적어야 이겨요.

💭 [생각해볼 주제 예시답안]

1. for 루프가 멀쩡한데 왜 map·filter를 쓸까?

문제 상황 요약

지난 시간 for 루프로도 게시물을 잘 걸러냈어요. 동작은 똑같은데 오늘은 filter·map으로 바꿨죠. 코드를 읽는 사람 입장에서, 둘 중 어느 쪽이 "무슨 일을 하는지" 한눈에 들어올까요?

튜터의 가이드 및 해설

배열 메서드의 가치는 "어떻게 도는가를 감추고, 무엇을 하는가만 남긴다" 에서 나와요.

for 루프로 게시물을 거른다고 해볼게요. let result = []을 만들고, let i = 0부터 시작해, i < items.length 조건을 확인하고, i++로 늘리고, items[i]로 꺼내고, if로 조건을 보고, 맞으면 result.push... 이 모든 절차를 매번 직접 적어야 해요. 그런데 이 중 대부분은 "거른다"는 목적과 직접 상관없는 반복 절차예요. 정작 중요한 "무엇을 거르는가"는 if 조건 한 줄에 묻혀 있죠.

filter로 쓰면 feed.filter((item) => item.category === "travel") 한 줄이에요. i를 늘리고 배열에서 꺼내는 절차는 filter가 안에서 알아서 해요. 우리는 "무엇을 거를지"(category === "travel")만 적으면 되죠. 코드를 읽는 사람도 "아, 여행 게시물을 거르는구나"가 바로 보여요. map도 마찬가지예요. "각 요소를 무엇으로 바꾸는가"만 남고, 도는 절차는 감춰지죠.

정리하면 이렇게 갈려요.

  • for 루프: 도는 절차(i, 조건, 증가, 꺼내기)를 매번 직접 적어야 해서, 진짜 목적이 절차에 묻혀요.
  • map·filter: 도는 절차는 메서드가 감추고, "무엇을 하는가"만 콜백에 남아 한눈에 들어와요.
  • 그래서 보통은: 단순한 순회·변환·걸러내기는 배열 메서드를 써요. 의도가 코드에 그대로 드러나거든요.

🎯 면접관을 홀리는 핵심 멘트

"for 루프와 filter는 결과가 같지만, 읽는 사람에게 주는 정보가 달라요. for 루프는 인덱스를 늘리고 배열에서 꺼내는 절차가 다 드러나서, 정작 '무엇을 거르는지'가 묻혀요. filter는 도는 절차를 감추고 조건만 남기죠. 저는 의도가 코드에 바로 드러나는 쪽을 선호해서, 단순한 순회·변환은 배열 메서드를 씁니다."


2. 객체는 배열과 무엇이 다르고, 언제 쓸까?

문제 상황 요약

게시물을 ["제주 여행", "travel", 120] 배열로 둘 수도 있고, { caption: "제주 여행", category: "travel", likeCount: 120 } 객체로 둘 수도 있어요. 어떤 데이터일 때 배열이 어울리고, 어떤 데이터일 때 객체가 어울릴까요?

튜터의 가이드 및 해설

배열과 객체의 갈림길은 "순서가 중요한가, 종류가 다른 값들의 묶음인가" 예요.

게시물을 ["제주 여행", "travel", 120] 배열로 두면, 값을 꺼낼 때 post[0], post[1], post[2]라고 번호로 접근해요. 문제는 "0번이 글이고, 1번이 카테고리고, 2번이 좋아요"라는 걸 사람이 외워야 한다는 거예요. 나중에 게시물에 작성 시간을 추가하려고 중간에 값을 끼우면, 번호가 다 밀려서 기존 코드가 줄줄이 깨지죠. 종류가 다른 값들을 순서로 묶는 건 위태로워요.

객체로 { caption, category, likeCount }라고 두면, post.caption, post.category처럼 이름표로 꺼내요. 순서를 외울 필요가 없고, 이름만 알면 되죠. 새 정보를 추가해도 기존 코드는 안 깨져요. post.createdAt을 새로 넣어도 post.caption은 그대로니까요. 종류가 다른 값들의 묶음에는 객체가 훨씬 안전하고 읽기 좋아요.

반대로 배열이 어울리는 데도 있어요. 스토리에 올라온 친구 목록 ["jaehoon", "minji", "seungwoo"]처럼, 같은 종류의 값이 순서대로 늘어선 데이터죠. 여기선 순서 자체가 의미 있고(먼저 올린 순), 몇 개가 들어올지도 정해지지 않았어요. 이런 건 배열이 딱이에요.

정리하면 이래요.

  • 배열: 같은 종류의 값이 순서대로 늘어선 데이터(친구 목록, 좋아요 숫자들). 순서가 의미 있고 개수가 유동적일 때.
  • 객체: 종류가 다른 값들을 한 덩어리로 묶은 데이터(게시물 하나, 사용자 하나). 이름표로 꺼내야 의미가 분명할 때.
  • 그래서 보통은: 게시물 하나는 객체로, 게시물 여러 개는 객체들의 배열로 둬요. 오늘 만든 feed가 바로 그 모양이죠.

🎯 면접관을 홀리는 핵심 멘트

"배열은 같은 종류의 값이 순서대로 늘어선 데이터에, 객체는 종류가 다른 값들의 묶음에 어울려요. 게시물을 배열로 두면 '몇 번이 무슨 값'인지 외워야 하고 중간에 값을 추가하면 번호가 밀려 깨지죠. 객체로 두면 이름표로 꺼내니 안전하고 의미도 분명해요. 그래서 게시물 하나는 객체로, 여러 개는 객체들의 배열로 두는 걸 기본으로 삼습니다."


3. 원본을 바꾸는 것과 새로 만드는 것, 어느 쪽이 안전할까?

문제 상황 요약

post.likeCount = 121은 원본을 직접 바꿨고, 스프레드는 원본을 두고 새 객체를 만들었어요. 둘 다 좋아요를 올리는 결과는 같아요. 여러 코드가 같은 데이터를 함께 본다면, 어느 쪽이 안전할까요?

튜터의 가이드 및 해설

둘의 갈림길은 "값이 언제 어디서 바뀌는지 추적할 수 있는가" 예요.

원본을 직접 바꾸는 방식(post.likeCount = 121)을 생각해볼게요. 게시물 하나를 화면에 그리는 코드, 좋아요 수를 세는 코드, 알림을 보내는 코드가 같은 게시물 객체를 함께 본다고 해봐요. 그런데 한 곳에서 post.likeCount를 슬쩍 바꿔버리면, 나머지 코드도 자기도 모르게 바뀐 값을 보게 돼요. 화면엔 120인데 알림은 121을 쓰는 식의 엇갈림이 생기죠. 값이 어디서 바뀌었는지 추적하기도 어려워요. 모두가 같은 객체를 공유하니까요.

스프레드로 새 객체를 만드는 방식({ ...post, likeCount: 121 })은 원본을 안 건드려요. 좋아요를 올린 새 게시물을 따로 만들고, 원본은 그대로 두죠. 그러면 "예전 상태"와 "바뀐 상태"가 둘 다 남아서, 무엇이 어떻게 달라졌는지 비교할 수 있어요. 원본을 보던 코드는 영향을 안 받고요. 값이 바뀌는 지점이 "새 객체를 만드는 그 순간"으로 분명해져요.

물론 원본을 직접 바꾸는 게 더 간단하고, 늘 나쁜 건 아니에요. 작고 혼자 쓰는 데이터라면 직접 바꿔도 괜찮죠. 하지만 여러 코드가 데이터를 공유할수록, 원본을 지키며 새 걸 만드는 방식이 사고를 줄여줘요.

정리하면 이래요.

  • 원본 직접 바꾸기: 간단하지만, 공유하는 데이터면 다른 코드가 모르게 영향을 받고 추적이 어려워요.
  • 새로 만들기(스프레드): 원본이 안전하고, 예전·바뀐 상태가 둘 다 남아 비교·추적이 쉬워요.
  • 그래서 보통은: 여러 곳이 함께 보는 데이터일수록 원본을 지키고 새 걸 만들어요. 지난 시간 클로저로 상태를 보호한 것과 같은 고민이에요.

🎯 면접관을 홀리는 핵심 멘트

"원본을 직접 바꾸면 그 데이터를 공유하는 다른 코드가 모르게 영향을 받고, 값이 어디서 바뀌었는지 추적하기 어려워져요. 스프레드로 새 객체를 만들면 원본은 안전하고, 예전 상태와 바뀐 상태가 둘 다 남아 비교할 수 있죠. 그래서 여러 곳이 함께 보는 데이터일수록 원본을 지키고 새로 만드는 방식을 택합니다."

더 배우려면

실무 프로젝트까지 가고 싶다면

팀스파르타 백엔드 부트캠프에서 인스타그램 클론을 풀스택으로 완성합니다.