프론트Vue3
5

얕은 복사 vs 깊은 복사

얕은 복사로 "배열 껍데기는 분리, 안의 데이터는 공유" 전략을 사용해 해결했다.

얕은 복사 vs 깊은 복사

확인 버튼을 누르기 전에 부모 데이터가 바뀌는 버그, 원인은 복사 방식에 있었다.


문제 상황

첨부파일 팝업을 열고, 자식 컴포넌트에서 파일을 삭제했을 때
확인 버튼을 누르기 전인데도 부모의 데이터가 즉시 변경되는 버그가 발생했다.

// 팝업 오픈 시 — 문제 코드
const params = {
  props: {
    savedFiles : state.savedFiles,    // ⚠️ 참조값 그대로 전달
    newFiles   : state.newFiles,      // ⚠️ 참조값 그대로 전달
    deleteFiles: state.deleteFiles,   // ⚠️ 참조값 그대로 전달
  }
};

원인 — 자바스크립트의 참조 전달

자바스크립트에서 배열/객체를 변수에 담으면
실제 데이터가 아닌 메모리 주소(참조값) 를 담는다.

const 원본 = { name: "철수" }
const 복사 = 원본         // 주소를 복사한 것

복사.name = "영희"
console.log(원본.name)   // "영희" ← 원본도 바뀜!

현재 코드의 흐름

rowFileState (부모 원본)
       │
       └──► savedFiles ──► [ 파일A, 파일B ]  ← 실제 데이터
                                  ▲
                           같은 주소를 가리킴
                                  │
              자식 팝업에 넘긴 savedFiles ──┘

// 자식이 파일 삭제하면?
// → 부모 원본도 동시에 바뀜 💥

3가지 복사 방식 완전 비교

구조 이해

① 참조 (그냥 넘기기)

savedFiles: state.savedFiles
state.savedFiles ───────▶ [배열]
                              ▲
next.savedFiles  ─────────────┘
// 같은 배열을 공유 → 둘 중 하나 바꾸면 같이 바뀜

② spread (얕은 복사)

savedFiles: [...state.savedFiles]
state.savedFiles ─────▶ [배열]    ─▶ { name: "파일A" }
                                            ▲
next.savedFiles  ─────▶ [새 배열] ──────────┘
// 배열은 새로 생성, 내부 객체는 여전히 공유

③ JSON (깊은 복사)

savedFiles: JSON.parse(JSON.stringify(state.savedFiles))
state.savedFiles ─────▶ [배열]    ─▶ { name: "파일A" }

next.savedFiles  ─────▶ [새 배열] ─▶ { name: "파일A" }  ← 완전히 별개
// 배열도, 내부 객체도 전부 새로 생성

실제 동작 차이

const state = {
  savedFiles: [{ name: 'a.png' }]
}

① 참조

const next = { savedFiles: state.savedFiles }

next.savedFiles.push({ name: 'b.png' })
console.log(state.savedFiles) // [{ name: 'a.png' }, { name: 'b.png' }] 같이 변경 💥

② spread

const next = { savedFiles: [...state.savedFiles] }

next.savedFiles.push({ name: 'b.png' })
console.log(state.savedFiles) // [{ name: 'a.png' }] 영향 없음 ✅

// BUT 내부 객체는 공유
next.savedFiles[0].name = 'c.png'
console.log(state.savedFiles[0].name) // 'c.png' 같이 변경 💥

③ JSON

const next = { savedFiles: JSON.parse(JSON.stringify(state.savedFiles)) }

next.savedFiles[0].name = 'c.png'
console.log(state.savedFiles[0].name) // 'a.png' 완전히 분리 ✅

한눈에 비교

방식배열 변경내부 객체 변경File 객체 보존특징
참조같이 바뀜 💥같이 바뀜 💥대부분 버그 원인
spread안 바뀜 ✅같이 바뀜 💥대부분 상황에서 적합
JSON안 바뀜 ✅안 바뀜 ✅💥 소실완전 복사, File 주의

JSON 깊은 복사를 쓰면 안 되는 이유

newFiles에는 실제 File 객체(바이너리)가 포함되어 있다.

// newFiles 안의 실제 데이터
File { name: "영수증.jpg", size: 204800, type: "image/jpeg", ... }

// JSON 변환 시
JSON.stringify(File객체)  →  '{}'   // 빈 객체로 변환 💥
JSON.parse('{}')          →  {}    // 파일 데이터 전부 소실

실제 발생한 버그 시나리오

1. 행추가 → newFiles: []  (빈 배열)

2. 팝업 오픈 → 파일 추가
   AttachFileComponent 내부: newFiles = [ File { name: "영수증.jpg" } ]

3. 확인 버튼 클릭
   rowFileState[rowId].newFiles = [ File { name: "영수증.jpg" } ]  ← 정상 저장

4. 팝업 재오픈 → JSON 복사 실행
   JSON.stringify(File객체) = '{}'
   JSON.parse('{}') = {}
   → newFiles: [{}]  ← 💥 파일 데이터 소실

5. AttachFileComponent에 [{}] 전달
   → 기존 첨부파일 데이터 안 보임 💥

deleteFiles는 일반 객체라 JSON 복사가 정상 동작하기 때문에
"파일 삭제는 되는데 파일 추가만 안 됨" 같은 원인 찾기 어려운 버그로 이어진다.


해결 전략 — "배열 껍데기는 분리, 안의 데이터는 공유"

이 케이스에서 필요한 건 딱 하나다.
자식이 배열에 push/splice 해도 부모 원본 배열에 영향 없게 하는 것.

[...state.newFiles]

새 배열 ──► [ 주소1, 주소2 ]
                │
                └──► File { name: "영수증.jpg", ... }  ← 같은 객체 바라봄
  • 배열은 새로 만들어서 → 자식이 push/splice 해도 원본 배열에 영향 없음 ✅
  • File 객체는 같은 곳을 바라봐서 → 실제 파일 데이터는 그대로 유지 ✅

자식 컴포넌트에서 하는 작업은 배열에 넣고 빼는 것뿐이고
File 객체 내부를 직접 수정하지 않기 때문에 참조를 공유해도 안전하다.
오히려 복사하면 File 데이터가 소실되므로 복사하면 안 된다.

// 팝업 오픈 시 — 해결 코드
const params = {
  props: {
    atchFileId : detailForm.value.atchFileId || '',
    savedFiles : [...state.savedFiles],    // ✅ 배열만 새로 생성, 객체는 공유
    newFiles   : [...state.newFiles],      // ✅ File 인스턴스 안전하게 보존
    deleteFiles: [...state.deleteFiles],   // ✅ 배열만 새로 생성, 객체는 공유
  }
};

언제 어떤 방식을 써야 하나

// ✅ 참조 — 상태를 공유해야 할 때만
savedFiles: state.savedFiles
// 대부분 버그의 원인이 되므로 주의

// ✅ spread — 배열 추가/삭제만 막으면 될 때 (기본값)
savedFiles: [...state.savedFiles]
// 파일 리스트, ID 리스트 등 대부분의 경우

// ✅ JSON — 내부 객체 프로퍼티까지 완전히 분리해야 할 때
savedFiles: JSON.parse(JSON.stringify(state.savedFiles))
// 단, Date 타입 깨짐 / File 객체 소실 / 성능 저하 주의

수정 전후 동작 비교

시점수정 전수정 후
팝업 오픈원본 참조 전달복사본 전달
자식에서 파일 삭제부모 원본 즉시 변경 💥복사본만 변경 ✅
확인 버튼 클릭이미 반영돼서 의미 없음콜백으로 원본 업데이트 ✅
취소 버튼 클릭원본 이미 변경돼 복구 불가 💥원본 유지 ✅

핵심 정리

  • 배열/객체를 그대로 넘기면 주소(참조) 를 공유하게 됨
  • 자식이 수정하면 부모 데이터도 동시에 변경됨
  • [...배열] 는 "배열 껍데기는 분리, 안의 데이터는 공유" → 이 케이스의 정확한 해결 전략
  • JSON.parse(JSON.stringify(...)) 는 File 객체를 소실시키므로 이 케이스에 부적합
  • 어떤 복사 방식을 쓸지는 "무엇을 수정하느냐" 에 따라 결정

댓글

(0)