값에 의한 전달, 참조에 의한 전달에 대해 알아보자.

박승주·2025년 4월 19일

컴퓨터 과학

목록 보기
2/2

주제를 다루게 된 이유

팀 프로젝트를 하던 도중 API에서 가져온 project 객체 배열과 member 객체 배열을 병합해야 하는 일이 생겼다. 그래서 아래와 같이 객체 배열들을 병합하는 함수를 작성했다.

// API에서 가져온 project 객체 배열
const PROJECTS = [
  {
    id: 1,
    title: '딸기 샌드위치 레시피',
    description: '딸기와 물엿을 이용한 손쉬운 샌드위치를 가정에서 만들어보세요!'
  },
  {
    id: 2,
    title: '삼겹살 김밥 마는법',
    description: '삼겹살 + 김치 + 밥으로 탄단지 영양소 밸런스와 맛을 모두 챙기는 법'
  },
  {
    id: 3,
    title: '[노오븐] 떠먹는 아이스박스 케이크',
    description: '오레오만 있다면 오븐 없이 누구나 손쉽게 케이크 베이킹 가능'
  },
]

// API에서 가져온 member 객체 배열
const MEMBERS = [
  {
    id: 1,
    author: '박딸기'
  },
  {
    id: 2,
    author: '김마리'
  },
  {
    id: 3,
    author: '이제빵'
  },
]

const mergeProjectsAndMembers = () => {
  const projectsWithMembersMap = new Map();
  [...PROJECTS, ...MEMBERS].forEach(ProjectMemberUnion => {
    if(projectsWithMembersMap.has(ProjectMemberUnion.id)) {
      // Map에 들어있던 project 객체 불러오기
      const project = projectsWithMembersMap.get(ProjectMemberUnion.id);

      // 불러온 project 객체와 member 객체 병합
      const projectWithMember = Object.assign(project, ProjectMemberUnion);
      projectsWithMembersMap.set(ProjectMemberUnion.id, projectWithMember);
    } else {
      projectsWithMembersMap.set(ProjectMemberUnion.id, ProjectMemberUnion);
    }
  })

  // Map을 value값으로만 이루어져 있는 배열로 변환하는 과정...
}

mergeProjectsAndMembers();

위에서 작성한 mergeProjectAndMembers 함수는 충분히 잘 동작한다. 그런데 코드 리뷰 시간에 동료 개발자로부터 개선이 필요하다는 진단을 받았다. 내가 보기에는 아무런 문제가 없어 보이는데 말이다. 그래서 해당 함수를 개선해보면서 제목에 적혀 있는 주제에 대해 탐구하려고 한다.

변수가 메모리에 저장되는 과정

대부분의 프로그래밍 언어는 변수의 선언과 할당의 과정을 거친다. 변수의 선언은 메모리에 공간을 추가하여 데이터를 저장할 준비를 한다. 그리고 변수의 할당은 추가된 메모리 공간에 데이터를 저장하는 것이다.

그런데 메모리를 할당하는 방식은 변수가 어떤 타입이냐에 따라 달라진다. 일명 원시 타입이라고 불리우는 숫자, 문자, null, undefined 등의 타입은 위에서와 같이 실제 데이터 값을 저장한다.
반면 자바스크립트에서 원시 타입을 제외한 타입을 참조 타입이라 부른다. 참조 타입은 객체나 배열처럼 생성 시점까지 크기를 알 수 없는 경우가 있기 때문에 동적 메모리 할당이 가능한 Heap 구조에 저장된다. 그래서 참조 타입은 Heap 구조에 객체를 저장하고 이 객체를 가리키는 참조값이 메모리에 저장된다.
아래 이미지를 통해 참조 타입이 메모리에 저장되는 방식을 쉽게 알 수 있다.

값에 의한 전달

만약 변수의 값이 변경되었다면 예상하기로는 기존 메모리의 데이터를 변경한다고 생각할 것이다. 그러나 기존 메모리의 데이터를 변경한다면 불변성을 위반하여 사이드 이펙트가 발생할 수 있다.
그래서 원시 타입은 변수의 값이 변경되었을 때 새 메모리 공간을 할당하여 변경된 데이터의 값을 저장한다.

let x = 10;
x = 20;
let y = 20;
let z = x + y; (z = 40);

참조에 의한 전달

참조 타입 값이 변경되었다면 메모리에 저장되어 있는 Heap의 참조값은 유지되고 Heap 안에 있는 객체가 변경된다.

let person = {
    name: '김철수'
    age : 24,
}

person.hobby = '독서'
person.job = '프론트엔드 개발자'

참조에 의한 전달 현상으로 인한 불변성 위반

객체와 같은 참조 타입의 값을 변경할 때 원본 데이터의 값을 변경해 예기치 않은 버그가 발생하는 경우가 있다. 아래 예시 코드에는 속성 하나를 삭제하는 함수를 호출하고 나서 각각의 속성을 Validation 하는 함수를 호출한다.

function setPublicUserForm(form) {
  const publicUserForm = form;
  delete publicUserForm.password;
  console.log('공개 회원 정보', publicUserForm);
}

function validateUser(form) {
  if(form.userId.length > 20) console.log('아이디의 길이가 너무 깁니다.');
  if(form.password.length < 6) console.log('비밀번호의 길이는 6자 이상으로 설정해 주세요.');
  ...
  console.log('검증이 끝난 회원 정보', form);
}

const userForm = {
  userId: 'happy1025',
  password: '1234asdf',
  email: 'happy1025@gmail.com',
  job: 'developer',
}


setPublicUserForm(userForm);
validateUser(userForm);

원래 참조에 의한 전달 개념을 몰랐다면 검증이 끝난 회원 정보는 모든 객체의 속성을 가진 채로 콘솔에 출력됨을 예상했을 것이다. 물론 그렇지 않고 검증이 끝난 회원 정보를 콘솔에 출력하는 위치에서는 password 속성이 삭제되었기 때문에 에러가 발생했다.

TypeError: Cannot read properties of undefined (reading 'length')

변수를 함수의 매개변수로 전달할 때 원본 데이터를 가져오지 않고 변수의 데이터를 복사된 값을 가져온다. 즉, 객체를 함수의 매개변수로 전달할 때 객체를 가리키던 참조값을 복사하여 가져온다는 것이다.

그래서 password 속성을 삭제할 때 매개변수로 받은 객체뿐만 아니라 원본 객체의 password 속성도 사라져 검증 과정에서 password 속성을 찾지 못한다.

얕은 복사 vs 깊은 복사

이전의 예시처럼 매개변수로 사용하기 위해 객체를 복사하는 방식을 얕은 복사라고 부른다. 얕은 복사는 메모리의 참조 주소값만 복사하기 때문에 메모리를 적게 사용하지만 아까의 예시처럼 사이드 이펙트가 발생하거나 React 앱에서 불변성을 위반하여 리렌더링이 제대로 동작하지 않을 수 있기 때문에 신중히 사용해야 한다.
깊은 복사는 객체의 실제 값을 메모리 공간에 복사하는 방식이다. 때문에 원본 객체의 참조 주소가 다르며 복사된 객체에 수정, 삭제가 이루어져도 원본 객체에 영향을 끼치지 않는다.

자바스크립트 얕은 복사와 깊은 복사의 예시는 다음과 같다.

얕은 복사깊은 복사
Object.assign({}, object)JSON.stringify()
스프레드 연산자 ({...person})재귀 함수를 이용한 복사
Array.slice()loadsh 라이브러리의 deepCopy

깊은 복사를 사용하여 사이드 이펙트 방지

이전의 예시의 에러를 해결하기 위해서는 매개변수로 받은 객체를 깊은 복사 방식으로 복사하면 된다. publicUserForm은 깊은 복사 방식으로 복사되었으므로 userForm의 참조 주소와 다른 새 참조 주소 값으로 메모리에 할당된다.

function setPublicUserForm(form) {
  const publicUserForm = JSON.parse(JSON.stringify(form));
  delete publicUserForm.password;
  console.log('공개 회원 정보', publicUserForm);
}

function validateUser(form) {
  if(form.userId.length > 20) console.log('아이디의 길이가 너무 깁니다.');
  if(form.password.length < 6) console.log('비밀번호의 길이는 6자 이상으로 설정해 주세요.');
  console.log('검증이 끝난 회원 정보', form);
}

const userForm = {
  userId: 'happy1025',
  password: '1234asdf',
  email: 'happy1025@gmail.com',
  job: 'developer',
}


setPublicUserForm(userForm);
validateUser(userForm);

처음으로 돌아가서...

이제 값에 의한 전달과 참조에 의한 전달의 개념을 익혔으니 다시 처음 예제의 코드의 개선 방법을 알아보자.

// API에서 가져온 project 객체 배열
const PROJECTS = [
  {
    id: 1,
    title: '딸기 샌드위치 레시피',
    description: '딸기와 물엿을 이용한 손쉬운 샌드위치를 가정에서 만들어보세요!'
  },
  {
    id: 2,
    title: '삼겹살 김밥 마는법',
    description: '삼겹살 + 김치 + 밥으로 탄단지 영양소 밸런스와 맛을 모두 챙기는 법'
  },
  {
    id: 3,
    title: '[노오븐] 떠먹는 아이스박스 케이크',
    description: '오레오만 있다면 오븐 없이 누구나 손쉽게 케이크 베이킹 가능'
  },
]

// API에서 가져온 member 객체 배열
const MEMBERS = [
  {
    id: 1,
    author: '박딸기'
  },
  {
    id: 2,
    author: '김마리'
  },
  {
    id: 3,
    author: '이제빵'
  },
]

const mergeProjectsAndMembers = () => {
  const projectsWithMembersMap = new Map();
  [...PROJECTS, ...MEMBERS].forEach(ProjectMemberUnion => {
    if(projectsWithMembersMap.has(ProjectMemberUnion.id)) {
      // Map에 들어있던 project 객체 불러오기
      const project = projectsWithMembersMap.get(ProjectMemberUnion.id);

      // 불러온 project 객체와 member 객체 병합
      const projectWithMember = Object.assign(project, ProjectMemberUnion);
      projectsWithMembersMap.set(ProjectMemberUnion.id, projectWithMember);
    } else {
      projectsWithMembersMap.set(ProjectMemberUnion.id, ProjectMemberUnion);
    }
  })

  // Map을 value값으로만 이루어져 있는 배열로 변환하는 과정...
}

mergeProjectsAndMembers();

프로젝트와 멤버 배열을 병합하는 과정에서 Object.assign이라는 얕은 복사 방식을 사용했다. 우리는 얕은 복사를 사용하면 복사된 객체에 수정, 삭제가 이루어지면 원본 객체에도 영향을 끼친다는 것을 알았다. 그러면 if 조건문에서 병합한 객체를 set 하는 동작을 생략해도 된다.

  if(projectsWithMembersMap.has(ProjectMemberUnion.id)) {
      // Map에 들어있던 project 객체 불러오기
        const project = projectsWithMembersMap.get(ProjectMemberUnion.id);
		Object.assign(project, ProjectMemberUnion)
    } else {
      projectsWithMembersMap.set(ProjectMemberUnion.id, ProjectMemberUnion);
    }

기존의 코드와 비교하면 단 한 줄의 코드만 생략하면서 이게 맞나? 생각이 들 수도 있다. 그렇지만 객체가 점점 복잡해지면 한 번 동작을 반복하는 것 만으로도 성능에 영향을 끼칠 수 있기 때문에 동료 개발자 분이 말씀해 주신대로 개선이 필요했다.

마치며

지금까지는 얕은 복사는 쓰지 말고 깊은 복사 방식만 사용해! 라고 머릿속으로 기억만 하고 있었지만 더 정확한 개념을 학습하며 각각의 방식을 사용하는 이유를 알게 되었다. 물론 얕은 복사 방식은 React와 같은 라이브러리의 경우 웹 동작에 예상 할 수 없는 이슈를 남길 수도 있으니 상황에 따라 적절한 방식을 사용해야 할 것이다.

profile
지금은 프론트엔드, 후에는 웹 얼리어답터가 꿈인 신입입니다.

0개의 댓글