본문 바로가기
Dev/🟨 JavaScript

[JS] 데이터를 다루는 객체 사용 방법 (feat. 전개연산자 (ES6), Trouble Shooting)

by 아아덕후 2023. 4. 10.
반응형
목차
1. 코드로서의 객체 생성 (객체 리터럴) 
2. 함수를 이용한 객체 생성
3. 클래스를 이용한 객체 생성
4. 객체 속성 추가 및 삭제
5. ★ 객체 복사 (참조 끊기) 및 분해 ★ (feat. Spread Operator,  Destructuring assignment)
6. [ES6] Spread Operator를 이용한 Trouble Shooting

1. 코드로서의 객체 생성 (객체 리터럴) 

let box = {
  width: 200,
  height: 200,
  borderRadius: 5,
  backgroundColor: "red",
};

 

코드로서의 객체는 위와 같이 객체내에 프로퍼티를 정의합니다.

해당 방법이 객체를 만드는데 가장 많이 사용되는 방법이지만, 단점도 굉장히 많습니다.

 

객체는 기본적인 (구성 or 모델 or 스킴 or타입이라는 다양한 표현)을 갖고 있으며 해당 에 맞춰 데이터가 들어가는 구조입니다.

해당 객체 리터럴로 생성한 방식은 틀과 데이터가 그대로 대입됩니다.

따라서 과 데이터가 함께 묶여있어 분리될 수 없는 구조입니다.

이 점을 보완하기 위해(과 데이터를 분리하여 사용할 수 있도록) 함수를 이용한 객체를 만드는 방법을 사용할 수 있습니다.

 


2. 함수를 이용한 객체 생성

 

function makeBox(width, height, borderRadius, backgroundColor) {
  return {
    width: width,
    height: height,
    borderRadius: borderRadius,
    backgroundColor: backgroundColor,
  };
}

const boxByFuntion = makeBox(100, 100, 10, "black");
console.log("boxByFuntion", boxByFuntion);

//-------
//[출력]
// boxByFuntion :  {width: 100, height: 100, borderRadius: 10, backgroundColor: 'black'}

함수를 이용해서 객체를 생성하는 방식은 위와 같이 함수 호출 시,

인자를 받아 해당 인자가 들어간 Box 객체를 return 하는 방법입니다.


makeBox의 함수를 이용한 return 값으로 객체 생성 시, 객체리터럴의 방법은 동일하지만,

직접적으로 객체에 데이터가 드러나지 않을 수 있도록 변수를 사용할 수 있습니다.

다시 말하면, 객체의 (속성, 프로퍼티)과 데이터를 분리할 수 있습니다.

 

1번 객체 리터럴과 다른 점으로 틀과 데이터를 분리하는 것이 함수를 이용해서 객체를 만드는 이유입니다.

 

함수를 이용해서 객체를 만드는 방법을 사용하는 가장 큰 이유를 예시를 통해 설명하면 다음과 같습니다.
같은 객체에 개별 데이터들이 10만개 필요한 경우,

[2. 함수를 이용한 객체 생성] 구성 정보를 담고 있는 객체 속성 함수를 1개 선언 하여 10만개의 각 데이터를 각 함수를 통해 생성하면 됩니다. (10만 + 4개 = 10만 4개)
하지만, [1. 코드로서의 객체](객체 리터럴)로 구현하는 방법으로 개별 데이터 10만개를 구현하기 위해서는
객체 속성 개수(여기서는 4개) * 10만개  = 40만 줄의 코드 가 필요하게 됩니다.


이때, 객체 속성의 변경이 필요한 경우에 함수를 이용한 방법은 함수 내의 return 객체의 속성 1줄만 변경하면 되는데 반해

첫 번째 방법인 객체리터럴은 모든 객체 리터럴의 속성을 변경해주어야 하기 때문에 10만개를 변경해야 합니다.
이처럼 어마어마한 생산성의 차이가 발생할 수 있는 경우가 있습니다.

 


3. 클래스를 이용한 객체 생성 (ES6) 

class Shape {
  width;
  height;
  borderRadius;
  backgroundColor;
  constructor(width, height, borderRadius, backgroundColor) {
    this.width = width;
    this.height = height;
    this.borderRadius = borderRadius;
    this.backgroundColor = backgroundColor;
  }
}

const boxShape = new Shape(10, 10, 0, "blue");
console.log("boxShape : ", boxShape);

// ------
// [출력]
// boxShape :  Shape {width: 10, height: 10, borderRadius: 0, backgroundColor: 'blue'}

 

 

ES6의 클래스는 기존 프로토타입 기반 객체지향 프로그래밍보다 클래스 기반 언어에 익숙한 프로그래머가 보다 빠르게 학습할 수 있는 단순명료한 새로운 문법을 제시하고 있습니다.   (- 참고 : poiemaWeb )

 

클래스도 인스턴스라고 하는 객체를 만드는 문법 구성 요소이다.
클래스도 기본적으로 함수 기반이기 떄문에 함수로 선언한 것과 클래스로 선언한 것의 모양은 거의 비슷하다.
인스턴스 객체는 결국 객체이기 때문에 데이터로서의 구성만 가지고 있다면, 객체 데이터가 될  수 있다.

 

클래스를 이용한 객체 생성의 장점은 어떤 클래스로 만든 객체인지 확인해 볼 수 있는 기능때문이다.
위의 1. 코드로서의 객체 생성과 2. 함수를 이용한 객체 생성은 규격이 똑같다면 어떻게 만들었는지 알 수 있는 방법이 없다.
하지만 클래스를 이용한 인스턴스 객체는 명확하게 어떤 클래스로 인해 만들어졌는지 instanceOf연산자를 활용하여 확인할 수 있는 장점이 있다.

 

console.log(boxShape instanceof Shape ? "참" : "거짓");

// -------
// [출력]
// 참

4. 객체 속성 추가 및 삭제

객체 속성 추가는 객체.프로퍼티명  = "값"을 통해 이전에 있는/없는 속성을 할당할 수 있습니다.

// 객체 속성 추가
box.color = "black";
console.log("box  : ", box);
console.log("boxByFuntion : ", boxByFuntion);


// -------
// [출력]
// box  :  {width: 200, height: 200, borderRadius: 5, backgroundColor: 'red', color: 'black'}
// boxByFuntion :  {width: 100, height: 100, borderRadius: 10, backgroundColor: 'black'}

 

객체 속성 삭제는 delete 객체.프로퍼티명 을 통해 특정 프로퍼티(속성)을 삭제할 수 있습니다.

// 객체 속성 삭제
delete box.color;
console.log("====delete box.color===");
console.log("box  : ", box);
console.log("boxByFuntion : ", boxByFuntion);

// ------
// [출력]
// ====delete box.color===
// box  :  {width: 200, height: 200, borderRadius: 5, backgroundColor: 'red'}
// boxByFuntion :  {width: 100, height: 100, borderRadius: 10, backgroundColor: 'black'}

 


5. 객체 복사 (참조 끊기) 및 분해


객체는 참조 타입이기 때문에 변수에 기존 객체를 담은 변수를 할당하면,

새로운 객체가 아닌 기존 객체를 참조하게 된다.

javascript의 객체는 새로운 변수에 담을 때 할당이 아닌 참조가 됩니다.
예를 들어
const a = { color:'red' , width:100 } 객체가 있다고 가정할 때
const b = a 라고 하면 
b가 가리키는 것은 a의 객체 메모리 주소이기 때문에
b.color = 'blue' 를 할당하면,
a = { color:'blue' , width:100 } 로 변경 되는 참조가 발생합니다.

 


따라서 객체 원본을 바꿔야 하는 경우 = 참조를 끊어내야 하는 경우 = 객체를 복사하는 경우

다음과 같은 3가지 방법이 존재한다. 


1. Object가 제공하는 assign 메소드 이용 -> 첫 번째 인자에게 두번째 이후 인자들을 모두 오버라이트한다.
2. 전개연산자(ES6, spread Operator)를 이용해서 새로운 객체를 만들고 거기에 기존 객체를 모두 풀어 헤쳐 전개시킨 다음에 재구성하여 새로운 객체에다가 넣는 방법.
3. 복사하고자 하는 객체를 문자열로 만든 후 다시 문자열을 객체로 만든다. -> 원시적인 방법이지만, 확실한 방법입니다.

const boxCopy = box; // 객체 참조
const boxCopy1 = Object.assign({}, box); // 객체 복사
const boxCopy2 = { ...box }; // 객체 복사
const boxCopy3 = JSON.parse(JSON.stringify(box)); // 객체 복사

box.width = 500;
console.log("boxCopy:  ", boxCopy);
console.log("boxCopy1: ", boxCopy1);
console.log("boxCop2y: ", boxCopy2);
console.log("boxCo3py: ", boxCopy3);

// ---------
// [출력]
// boxCopy:   {width: 500, height: 200, borderRadius: 5, backgroundColor: 'red'}
// boxCopy1:  {width: 200, height: 200, borderRadius: 5, backgroundColor: 'red'}
// boxCop2y:  {width: 200, height: 200, borderRadius: 5, backgroundColor: 'red'}
// boxCo3py:  {width: 200, height: 200, borderRadius: 5, backgroundColor: 'red'}

 

 

ES6 문법으로 Destructuring assignment가 가능해졌습니다.

이는 객체, 배열을 분해하는 것으로 객체명.key이름 으로 접근해야 하는 방법을 사용하지 않고

대괄호 안에서 키 값으로 해당 객체 내의 value를 할당할 수 있습니다.

이때 key의 값으로 새로 짓고 싶은 변수명(여기서는 w2, h2)을 통해서

해당 key의 value 값을 다른 변수에 할당할 수 있습니다.

// boxCopy  =  {width: 500, height: 200, borderRadius: 5, backgroundColor: 'red'}

const w1 = boxCopy.width;
const h1 = boxCopy.height;

// Destructuring assignment
const { width, height } = boxCopy;
const { width: w2, height: h2 } = boxCopy;

console.log("w1, h1:", w1, h1);
console.log("w2, h2:", w2, h2);

// ------------
// 출력
// w1, h1: 500 200
// w2, h2: 500 200

 

 


6. [ES6] Spread Operator를 이용한 Trouble Shooting

[ISSUE]

 

페이지(1번 이미지)에서 띄운 요청 팝업창에서(2번 이미지) 띄운 검색 팝업(3번 이미지) 에서 AgGrid를 선택하면 
페이지(1번 이미지) 에서 클릭한 AgGrid row가 사라지는 현상에 대한 이슈 였습니다.

 

AgGrid 특징으로 선택 된 row에 데이터를 gridApi에 담고 있기 때문에 

각 AgGrid마다 gridAPI를 담을 변수가 필요합니다.

 

페이지

해당 페이지에서 흐름은 
(1) 페이지에서 하나의 row 선택 후 등록 버튼 ->  (2) 요청 Form 팝업의 검색 버튼-> 검색 팝업으로 진행되며

 

요청 팝업 / 검색 팝업 

(3) 검색 팝업에서 AgGrid로 구현 된 테이블에서 하나의 row를 클릭하여 선택 버튼 클릭 시, 

(4) 요청 Form 팝업으로 해당 데이터를 전달하여 Form에 데이터를 담습니다.

(5) 승인 요청 버튼을 클릭 한 후 API를 전송합니다.

이후 1번 페이지로 복귀했을 때, 이전에 선택 했던 row가 선택되어 있어야 하는데 선택이 해제 되있는 상황입니다.

 

[원인]

이때,  문제점은 1번 페이지에서 리스트를 클릭 할 때 클릭된 데이터(AgGrid)를

3번 이미지 검색 팝업에서 선택한 후 반환되는 데이터를 같은 객체에 담고 있었습니다.

이로 인해 검색 팝업(3번 이미지)에서 데이터를 선택함과 동시에 페이지(1번이미지)에서 선택한 데이터가 클릭 해제되는 이유였습니다.

 

코드로 살펴보면 , 

<script>
export default {
    ...
    methods:{
    	onClickAgGridRow(params) {
          this.onBindRowParams(params.api.getSelectedRows()[0]);
        },
        
        onBindRowParams(selectedRowData) {
          this.vuexState.EXAMPLE_OBJ = selectedRowData;
        },

        returnData(value) {
          this.vuexState.EXAMPLE_OBJ.Id = value.userId;
          this.vuexState.EXAMPLE_OBJ.Nm = value.userNm;
        },
        ...
    }
    ...
}
</script>

<template>
...
	<!-- 페이지 AgGrid -->
    <AgGridVue
      ...
      @rowClicked="onClickAgGridRow"
      ...
    >
    </AgGridVue>

...
	<!-- 검색 팝업 컴포넌트 -->
    <ModalSearch
      ...
      @returnData="returnData"
      ...
    >
    </ModalSearch>

...
</template>

가장 먼저 AgGridVue 라이브러리(컴포넌트)에서 rowClicked 이벤트가 실행되며

onClickAgGridRow() 함수에서 클릭 된 데이터를 바인딩하기 위해 onBindRowParams에 전달하여 vuexState의 EXAMPLE_OBJ 객체에 저장합니다.

 

이후 위의 로직이 진행하며 검색 팝업인 ModalSearch 컴포넌트에서 선택 된 값을 returnData(emit)로 반환하며, 

해당 함수에서 vuexStore에 저장 된 EXAMPLE_OBJ 객체의 값으로 저장하고 있습니다.

 

이때 onBindRowParams() 함수에서는 EXAMPLE_OBJ 객체자체를 선택 된 데이터를 담고있는 객체로 인식하지만,

검색 결과의 값이 객체에 추가됨에 따라 AgGrid는 해당 객체를 통해 선택 된 데이터를 인식할 수 없게 되어 클릭이 해제된 것이였습니다.

 

[해결방법] 

이를 해결하기 위해 Spread Operator를 사용했습니다.

페이지(1번 이미지) 데이터와 검색 팝업(3번 이미지) 데이터 모두 객체였기 때문에 이를 해결하기 위해서 
페이지에서 클릭 시 할당 되는 row Data와
검색 팝업에서 전송되는 returnData를 
각각 스프레드 연산자를 사용하여 selectedData = { ...RowData , ...returnData }로 담으며 
두 이벤트에 대해 서로 침범하지 않도록 했습니다.

<script>
export default {
    ...
    methods:{
    	onClickAgGridRow(params) {
          this.onBindRowParams(params.api.getSelectedRows()[0]);
        },
        
        onBindRowParams(selectedRowData) {
          this.vuexState.EXAMPLE_OBJ = {
            ...this.vuexState.EXAMPLE_OBJ,
            ...selectedRowData,
        },

        returnData(value) {
          this.vuexState.EXAMPLE_OBJ.Id = value.userId;
          this.vuexState.EXAMPLE_OBJ.Nm = value.userNm;
        },
        ...
    }
    ...
}
</script>

<template>
...
	<!-- 페이지 AgGrid -->
    <AgGridVue
      ...
      @rowClicked="onClickAgGridRow"
      ...
    >
    </AgGridVue>

...
	<!-- 검색 팝업 컴포넌트 -->
    <ModalSearch
      ...
      @returnData="returnData"
      ...
    >
    </ModalSearch>

...
</template>

변경 된 onBindRowParams() 함수는 전개 연산자를 통해 이전의 객체 값과 함께 선택 값을 함께 담을 수 있는 객체로 변경합니다.

this.vuexState.EXAMPLE_OBJ = {            
                           ...this.vuexState.EXAMPLE_OBJ,           
                           ...selectedRowData,
                           }

이를 통해 this.vuexState.EXAMPLE_OBJ가 참조하고 있는 객체와 연결을 끊고 

이전에 존재하던 값만을 복사하여 담은 후, 페이지에서 선택한 AgGrid Row Data를 추가로 담으며 새로운 객체를 생성할 수 있었습니다.

 

이를 통해 하나의 객체 내에서 두 곳의 데이터를 담아 이후 로직을 처리할 수 있게 되었습니다.


[참고]

패스트캠퍼스 - 김민태의 프론트엔드 아카데미 강의

https://poiemaweb.com/es6-class

https://developer.mozilla.org/ko/docs/Learn/JavaScript/Objects/Basics

 

 

반응형

댓글