카테고리 없음

이터레이터로부터 전개 구문으로 새 배열을 만드는 이유

lamarcK 2025. 3. 28. 14:04

1. 불변성 (Immutability) 유지 및 부수 효과 (Side Effect) 방지 (가장 중요)

📌 원본 배열 보호: 자바스크립트에서 배열은 객체(Object)이며, 참조 타입(Reference Type)이다. 변수에 배열을 할당하면 변수에는 배열 자체가 아니라 배열 데이터가 저장된 메모리 주소가 저장된다. 따라서 여러 변수가 동일한 배열을 가리킬 수 있다.

let originalArray = [1, 2, 3];
let anotherVar = originalArray; // anotherVar는 originalArray와 동일한 배열을 가리킴

anotherVar.push(4); // anotherVar를 통해 배열 수정

console.log(originalArray); // 출력: [1, 2, 3, 4] <- 원본 배열이 변경됨!
console.log(anotherVar);    // 출력: [1, 2, 3, 4]

 

만약 originalArray를 직접 사용하지 않고 복사본을 만들어 사용하면, 원본(originalArray)은 변경되지 않는다. 전개 구문은 이러한 복사본을 만드는 간편한 방법 중 하나이다.

let originalArray = [1, 2, 3];
let copiedArray = [...originalArray]; // 전개 구문으로 새 배열(복사본) 생성

copiedArray.push(4); // 복사본 배열 수정

console.log(originalArray); // 출력: [1, 2, 3] <- 원본 배열은 그대로 유지됨!
console.log(copiedArray);    // 출력: [1, 2, 3, 4]
  • 📌 예측 가능한 코드: 여러 곳에서 동일한 배열 참조를 공유하고 있을 때, 한 곳에서의 변경이 다른 곳에 예기치 않은 부수 효과(Side Effect)를 일으키는 것을 방지할 수 있다. 각 함수나 로직이 자신만의 데이터 복사본을 가지고 작업하면 코드를 이해하고 디버깅하기 쉬워진다.
  • 📌 상태 관리 라이브러리: React (상태 업데이트 시), Redux, Vuex 와 같은 상태 관리 라이브러리에서는 상태의 불변성을 중요하게 여긴다. 상태 변경 시 기존 상태 객체나 배열을 직접 수정하는 대신, 전개 구문 등을 사용하여 새로운 객체나 배열을 생성하는 방식으로 상태를 업데이트해야 변경 감지가 제대로 동작하고 상태 변화를 추적하기 용이하다.

✨ 2. 얕은 복사 (Shallow Copy) 생성

  • 📌 정의: 전개 구문은 배열의 얕은 복사(Shallow Copy)를 수행한다.
    • 배열의 요소가 원시 타입(Primitive Type: 숫자, 문자열, 불리언 등)이면 값을 그대로 복사해온다.
    • 배열의 요소가 객체 또는 배열(참조 타입)이면, 해당 객체/배열을 가리키는 메모리 주소(참조)만 복사한다. 즉, 원본 배열과 복사본 배열의 내부에 있는 객체/배열 요소는 동일한 객체/배열을 가리키게 된다.
  • ⚠️ 주의: 얕은 복사이기 때문에, 복사된 배열 내부의 객체나 배열을 수정하면 원본 배열에도 영향을 미친다.
    let originalArray = [1, { name: "Alice" }, [10, 20]];
    let shallowCopiedArray = [...originalArray];
    
    shallowCopiedArray[0] = 99; // 원시 타입 변경: 원본 영향 없음
    shallowCopiedArray[1].name = "Bob"; // 내부 객체의 속성 변경: 원본에도 영향 있음!
    shallowCopiedArray[2].push(30);   // 내부 배열 변경: 원본에도 영향 있음!
    
    console.log(originalArray);
    // 출력: [ 1, { name: 'Bob' }, [ 10, 20, 30 ] ]
    
    console.log(shallowCopiedArray);
    // 출력: [ 99, { name: 'Bob' }, [ 10, 20, 30 ] ]
    
    만약 내부 객체까지 완전히 분리된 복사본(깊은 복사, Deep Copy)이 필요하다면 전개 구문만으로는 부족하며, JSON.parse(JSON.stringify(obj)) 트릭(제약 있음)이나 lodash 라이브러리의 cloneDeep 함수 등을 사용해야 한다.

얕은 복사 : 배열 안의 배열, 객체는 메모리 주소만 복사 된다.

얕은 복사 (Shallow Copy) 생성 이라는것은 일반 배열은 그냥 전개구문을 쓰면 새 배열이 되지만(메모리 주소 다름) 배열안에 배열이 있는 것을 복사하면 배열 부분만 주소를 참조하게 된다는 소리인가?

 

네, 정확하게 이해했다! 그것이 바로 얕은 복사(Shallow Copy)의 핵심적인 특징이다.

조금 더 상세히 설명하면 다음과 같다.

 얕은 복사 상세 설명

  1. 새로운 최상위 배열 생성: 전개 구문 (...)을 사용하여 배열을 복사하면, 언제나 새로운 최상위(Top-level) 배열 컨테이너가 만들어진다. 이 새로운 배열은 원본 배열과는 다른 고유한 메모리 주소를 가지게 된다.
    let original = [1, 2, 3];
    let copied = [...original];
    
    // copied는 original과 다른 메모리 주소를 가진 새로운 배열이다.
    console.log(original === copied); // 출력: false
    
  2. 요소 복사 방식: 중요한 것은 이 새로운 배열 컨테이너 안으로 원본 배열의 요소들을 어떻게 복사해 넣느냐이다.
    • 원시 타입(Primitive Types) 요소 복사: 원본 배열의 요소가 숫자(number), 문자열(string), 불리언(boolean), null, undefined, symbol, bigint 와 같은 원시 타입일 경우, 그 값(Value) 자체가 그대로 복사되어 새 배열에 들어간다. 따라서 복사본 배열의 원시 타입 요소를 변경해도 원본 배열에는 아무런 영향이 없다.
      copied[0] = 99;
      console.log(original[0]); // 출력: 1 (원본 불변)
      console.log(copied[0]);   // 출력: 99
    • 참조 타입(Reference Types) 요소 복사: 원본 배열의 요소가 객체(Object)나 또 다른 배열(Array)과 같은 참조 타입일 경우, 그 객체나 배열 데이터 자체가 복사되는 것이 아니라, 해당 객체나 배열이 저장된 **메모리 주소(참조)**가 복사되어 새 배열에 들어간다.
      • 결과적으로, 원본 배열의 참조 타입 요소와 복사본 배열의 해당 요소는 똑같은 메모리 주소를 가리키게 된다. 즉, 동일한 객체 또는 배열 공유하게 되는 것이다.
  3. 결과: 이 때문에 배열 안에 중첩된 배열이나 객체가 있을 때, 복사본 배열을 통해 그 내부 배열이나 객체를 수정하면, 원본 배열을 통해 접근해도 변경된 내용이 보이게 된다. 왜냐하면 둘 다 결국 같은 내부 배열/객체를 바라보고 있기 때문이다.
    let originalNested = [1, [10, 20]];
    let copiedNested = [...originalNested];
    
    // copiedNested는 새로운 배열이지만, 내부 배열 [10, 20]은 원본과 동일한 것을 참조한다.
    console.log(originalNested[1] === copiedNested[1]); // 출력: true (내부 배열은 동일 참조)
    
    copiedNested[1].push(30); // 복사본의 내부 배열을 수정
    
    console.log(originalNested[1]); // 출력: [10, 20, 30] (원본의 내부 배열도 변경됨!)
    console.log(copiedNested[1]);   // 출력: [10, 20, 30]

📌 핵심 요약:

사용자가 이해한 내용이 정확하다.

  • 전개 구문은 항상 새로운 최상위 배열을 만든다 (주소 다름).
  • 내부 요소가 원시 타입이면 이 복사된다 (독립적).
  • 내부 요소가 참조 타입(배열, 객체 등)이면 **참조(메모리 주소)**가 복사된다 (원본과 공유).

이것이 "얕은 복사"라고 불리는 이유이다. 배열의 구조를 한 단계(Shallow)만 복사하고, 그 이상 깊이(Deep) 들어간 내부 객체/배열까지는 새로 만들지 않고 참조만 복사하기 때문이다.

 

배열 안의 객체는 매우 흔하게 사용된다.

✨ 중첩된 배열/객체의 사용 빈도

음, 그 생각은 실제 프로그래밍 상황과는 조금 다를 수 있다. 물론 숫자나 문자열만 담긴 단순한 배열도 많이 사용되지만, 배열 안에 객체(Objects in Array)가 있거나 배열 안에 또 다른 배열(Arrays in Array)이 있는, 즉 중첩된 구조(Nested Structure)는 생각보다 매우 흔하게 사용된다.

📌 배열 안의 객체 (Arrays of Objects) - 매우 일반적임

이 형태는 현대 웹 개발이나 데이터 처리에서 가장 흔하게 접하는 구조 중 하나이다.

  • 데이터 목록 표현: 데이터베이스(DB)나 API로부터 가져온 데이터 목록을 표현할 때 거의 항상 이 구조를 사용한다. 예를 들어, 사용자 목록, 상품 목록, 게시글 목록 등은 각각의 사용자, 상품, 게시글 정보를 담은 객체들의 배열로 표현된다.이런 users 배열을 복사할 때 전개 구문(...)을 사용하면, 배열 자체는 새로 만들어지지만 내부의 { id: ..., name: ... } 객체들은 원본과 동일한 참조를 가지게 된다 (얕은 복사). 만약 복사본의 사용자 객체 속성을 변경하면 원본에도 영향을 미친다.
    // 사용자 목록 예시
    const users = [
      { id: 1, name: "Alice", email: "alice@example.com", isActive: true },
      { id: 2, name: "Bob", email: "bob@example.com", isActive: false },
      { id: 3, name: "Charlie", email: "charlie@example.com", isActive: true }
    ];
    
  • UI 컴포넌트 데이터: 사용자 인터페이스(UI)의 리스트, 테이블, 드롭다운 메뉴 등에 표시될 항목들을 정의할 때도 각 항목의 속성(텍스트, 값, 아이콘, 활성화 상태 등)을 가진 객체의 배열을 사용하는 경우가 많다.
  • 설정(Configuration) 데이터: 여러 모듈이나 기능에 대한 설정을 객체로 정의하고, 이를 배열로 묶어 관리하기도 한다.

📌 배열 안의 배열 (Arrays of Arrays) - 특정 분야에서 흔함

이 형태는 위의 경우보다는 덜 보편적일 수 있지만, 특정 종류의 데이터를 다룰 때는 필수적으로 사용된다.

  • 2차원 데이터 (그리드, 행렬): 표(Table), 게임 보드판, 이미지 픽셀 데이터, 스프레드시트 데이터 등 행과 열로 구성된 2차원 구조를 표현할 때 자연스럽게 사용된다.
    // 3x3 게임 보드판 예시 (0: 빈칸, 1: O, 2: X)
    const gameBoard = [
      [0, 0, 0],
      [0, 1, 0],
      [2, 0, 0]
    ];
    
  • 수학 및 과학 계산: 행렬(Matrix) 연산이 필요한 선형대수 라이브러리 등에서 데이터를 표현하는 표준 방식이다.
  • 그룹화된 데이터: 데이터를 특정 기준에 따라 그룹화했을 때, 각 그룹에 속한 요소들을 담은 내부 배열들의 배열로 표현할 수 있다.
  • 그래프(Graph) 표현: 그래프 자료구조를 인접 리스트(Adjacency List) 방식으로 표현할 때 사용될 수 있다 (각 정점마다 인접한 정점들의 리스트를 배열로 가짐).

💡 결론:

배열 내부에 객체가 들어가는 경우는 매우 매우 흔하며, 사실상 현대 프로그래밍에서 데이터를 다루는 기본 방식 중 하나이다. 배열 내부에 배열이 들어가는 경우도 2차원 데이터나 특정 구조를 표현할 때 자주 사용된다.

따라서 얕은 복사(Shallow Copy)의 동작 방식과 그로 인해 발생할 수 있는 참조 공유 문제를 이해하는 것은 실제 개발에서 매우 중요하다. 전개 구문(...)이나 Array.prototype.slice() 같은 내장 메서드들이 얕은 복사를 수행한다는 점을 인지하고 있어야 예상치 못한 버그를 피할 수 있다.

 

배열 안의 객체 구조는 어떻게 활용되나?

배열 안의 객체(Objects in Array) 구조는 목록(List) 형태의 데이터를 표현하는 데 매우 유용하며, 각 목록 항목이 여러 개의 속성(Property) 또는 특성(Attribute)을 가질 때 표준적으로 사용되는 방식이다. 이 구조가 활용되는 주요 사례는 다음과 같다.

✨ 1. 데이터 목록 표현 (가장 일반적)

📌 API 응답 / 데이터베이스 결과

서버 API나 데이터베이스에서 여러 개의 레코드(Record)나 엔티티(Entity)를 조회한 결과를 클라이언트에게 전달할 때 가장 흔하게 사용된다. 각 객체는 하나의 레코드(예: 사용자 한 명, 상품 하나)를 나타내며, 객체의 속성은 해당 레코드의 필드(예: 이름, 이메일, 가격)에 해당한다.

// 예시: 상품 목록 데이터
const products = [
  { id: "p001", name: "노트북", price: 1200000, category: "electronics", inStock: true },
  { id: "p002", name: "키보드", price: 80000, category: "accessories", inStock: true },
  { id: "p003", name: "모니터", price: 350000, category: "electronics", inStock: false }
];
  • 목적: 여러 개의 구조화된 데이터 항목을 효율적으로 전달하고 관리한다.
  • 활용:
    • 서버로부터 JSON 형태로 데이터를 받아온다.
    • 받아온 데이터를 사용하여 UI에 목록(리스트, 테이블, 카드 등) 형태로 표시한다.
    • 사용자의 인터랙션(클릭, 검색, 필터링, 정렬)에 따라 데이터를 가공하여 표시한다.
    • 상태 관리 라이브러리(Redux, Vuex 등)에서 목록 상태를 저장하고 업데이트한다 (이때 불변성 유지가 중요하며, 얕은 복사에 주의해야 한다).

✨ 2. UI 컴포넌트 데이터 정의

📌 동적 목록/메뉴 생성

사용자 인터페이스(UI)에서 드롭다운 메뉴, 네비게이션 바, 탭 목록, 테이블 헤더 등을 동적으로 생성할 때 각 항목의 내용과 동작을 정의하기 위해 사용된다.

// 예시: 드롭다운 메뉴 옵션 정의
const dropdownOptions = [
  { label: "프로필 보기", value: "view_profile", icon: "user-icon" },
  { label: "설정", value: "settings", icon: "settings-icon" },
  { label: "로그아웃", value: "logout", icon: "logout-icon", disabled: false }
];
  • 목적: UI 요소의 구조와 내용을 데이터 기반으로 정의하여 코드의 반복을 줄이고 유지보수성을 높인다.
  • 활용:
    • 이 배열 데이터를 map 메서드 등으로 순회하며 각 객체 정보를 바탕으로 HTML 요소(예: <option>, <li>, <th>)를 동적으로 생성한다.
    • 각 항목의 value, disabled 등의 속성을 사용하여 이벤트 핸들링이나 조건부 렌더링을 구현한다.

✨ 3. 설정(Configuration) 데이터 관리

📌 다중 설정 항목 정의

애플리케이션의 여러 부분에 대한 설정이나 옵션을 구조화하여 관리할 때 사용된다. 각 객체는 특정 기능이나 모듈에 대한 설정을 담는다.

// 예시: 여러 차트 컴포넌트에 대한 설정
const chartConfigs = [
  { chartId: "salesChart", type: "line", title: "월별 매출", options: { color: "blue", smooth: true } },
  { chartId: "userChart", type: "bar", title: "가입자 통계", options: { color: "green", stacked: false } }
];
  • 목적: 관련 설정들을 하나의 배열로 묶어 관리의 용이성을 높이고, 설정을 기반으로 동적 처리를 가능하게 한다.
  • 활용:
    • 애플리케이션 시작 시 설정 파일을 읽어와 이 구조로 파싱한다.
    • 설정 배열을 순회하며 각 설정 객체에 따라 필요한 컴포넌트를 초기화하거나 동작 방식을 변경한다.

✨ 4. 데이터 처리 및 변환

📌 집계/분석/시각화용 데이터

데이터를 특정 기준에 따라 처리(예: map, filter, reduce 사용)하거나, 집계/분석한 결과를 저장할 때 사용된다. 특히 차트 라이브러리에 데이터를 전달하는 형식으로 자주 쓰인다.

// 예시: 월별 데이터 집계 결과
const monthlyData = [
  { month: "2025-01", value: 1500 },
  { month: "2025-02", value: 1750 },
  { month: "2025-03", value: 1600 }
];
  • 목적: 가공되거나 집계된 데이터를 명확한 속성(예: month, value)을 가진 객체의 배열로 표현하여 후속 처리(예: 시각화)를 용이하게 한다.
  • 활용:
    • 원시(Raw) 데이터를 가공하여 이 형태로 변환한다.
    • 차트 라이브러리(Chart.js, D3.js 등)에 입력 데이터로 전달하여 그래프를 그린다.
    • 테이블 형태로 데이터를 표시하거나 추가적인 분석을 수행한다.

✨ 5. 이벤트 및 로그 데이터

📌 순차적 발생 기록

사용자 행동 로그, 시스템 이벤트 로그 등 시간 순서에 따라 발생하는 이벤트들을 기록할 때 사용될 수 있다. 각 객체는 하나의 이벤트 정보를 담는다.

// 예시: 사용자 행동 로그
const userActions = [
  { timestamp: 1711543200000, type: "login", userId: "user1" },
  { timestamp: 1711543250000, type: "view_page", userId: "user1", page: "/dashboard" },
  { timestamp: 1711543300000, type: "click_button", userId: "user1", buttonId: "save" }
];
  • 목적: 발생한 이벤트들의 상세 정보(시간, 유형, 관련 데이터 등)를 구조적으로 저장하여 나중에 분석하거나 디버깅에 활용한다.
  • 활용:
    • 이벤트 발생 시 해당 정보를 객체로 만들어 배열에 추가한다.
    • 저장된 로그 데이터를 분석하여 사용자 패턴을 파악하거나 시스템 오류를 추적한다.

💡 결론:

"배열 안의 객체" 구조는 '여러 항목으로 구성된 목록이며, 각 항목은 여러 속성을 가진다' 는 종류의 데이터를 표현하는 데 매우 효과적이고 표준적인 방법이다. API 통신, UI 개발, 데이터 처리, 설정 관리 등 소프트웨어 개발의 거의 모든 영역에서 광범위하게 활용된다. 이 구조를 다룰 때 얕은 복사의 특성을 이해하는 것이 중요하다.

✨ 3. 함수형 프로그래밍 패턴 지원

  • 📌 순수 함수: 함수형 프로그래밍에서는 순수 함수(Pure Function)를 지향하는 경우가 많다. 순수 함수는 입력값을 변경하지 않고(No Side Effects), 동일한 입력에 대해 항상 동일한 출력을 반환한다. 함수 내부에서 입력으로 받은 배열을 직접 수정하는 대신, 전개 구문 등으로 복사본을 만들어 처리하고 새로운 배열을 반환하는 방식으로 순수 함수 조건을 만족시킬 수 있다.

✨ 4. 비파괴적인(Non-destructive) 요소 추가/변경

  • 📌 원본 유지: 기존 배열에 push, splice, sort 등의 메서드를 사용하면 원본 배열 자체가 변경(Mutate)된다. 전개 구문을 사용하면 원본은 그대로 두고, 요소가 추가되거나 변경된 새로운 배열을 쉽게 만들 수 있다.
    const original = [1, 2, 3];
    
    // 요소 추가 (비파괴적)
    const added = [...original, 4]; // [1, 2, 3, 4]
    console.log(original); // [1, 2, 3] (원본 유지)
    
    // 요소 중간 삽입 (비파괴적)
    const inserted = [...original.slice(0, 1), 99, ...original.slice(1)]; // [1, 99, 2, 3]
    console.log(original); // [1, 2, 3] (원본 유지)
    

💡 결론

전개 구문(...)을 사용하여 배열 복사본을 만드는 주된 이유는 원본 배열의 불변성을 지키고 예상치 못한 부수 효과를 방지하기 위함이다. 이는 특히 여러 곳에서 데이터가 공유되거나 상태 변화를 명확히 추적해야 하는 상황(예: 상태 관리, 함수형 프로그래밍)에서 매우 중요하다. 다만, 얕은 복사라는 점을 인지하고 사용해야 한다.