DEV Community

Jinseok
Jinseok

Posted on • Updated on

자바스크립트 원시 타입과 객체를 사용할 때 종종 실수하는 것

자바스크립트는 값을 두 가지로 분류한다. 원시 값primitive value과 객체object가 있다.

함수의 매개변수에서 값을 사용할 때 종종 내가 원하는 값이 나오지 않을 때가 있었는데, 이러한 특성을 몰랐었기 때문이다.

그리고 한창 리액트react가 유행하던 2017년 즈음엔 상태state 값으로 객체를 사용할 때 값을 변경해도 업데이트가 되지않는게 한창 이슈였는데 같은 맥락이다.


값을 변경할 수 없는 원시 값

변경할 수 없는걸 흔히 불변immutable이라 부른다. 원시 값은 이 불변이라는 조건을 가지게 된다.

하지만 다음 소스를 살펴보자

let fruitToBuy = "graph"
fruitToBuy = "apple"
console.log(fruitToBuy) // apple
Enter fullscreen mode Exit fullscreen mode

불변이라 말했지만 fruitToBuy 변수는 graph에서 apple로 변했다.

이는 apple 값을 가진새로운 string 타입의 값을 생성한 후 해당 변수에 할당한 것이다.


원시 값의 데이터 타입

원시 값에는 다음의 데이터 타입이 존재한다.
undefined, null, boolean, string, number, symbol(ECMAScript 2015), BigInt(ECMAScript 2020)

그러나 찝찝한 부분이 있다. 분명 숫자나 문자도 객체처럼 프로퍼티와 메소드를 가진다. 또한 문자열의 경우 배열이 아닌가? 구현이 어떻게 되었든 스펙에서는 원시 값으로 분류하고 있으니 더이상 객체로 생각하지 말자.


그렇다면 객체는 어떻게 변한다는 말일까?

객체 사용은 다음과 같다.

const obj = { jobTitle: "개발자" }
obj.greeting = "안녕"

console.log(obj) // { jobTitle: "개발자", greeting: "안녕"}
Enter fullscreen mode Exit fullscreen mode

흔히 사용하는 방식이다.

문제로 인식하기 시작하는 시점은 비교할 때이다.

obj === { jobTitle: "개발자", greeting: "안녕" } // false

{} === {} // false
Enter fullscreen mode Exit fullscreen mode

직관적으로 내용이 같으니 true가 될 것 같지만 아니다. 뭐라고 표현해야 좋을지 모르겠지만 객체는 (우리는 볼 수 없지만)자신이 가진 고유 식별자(C언어의 주소값 같은)를 비교한다.

다음의 예를 보자.

const objCopied = obj

console.log(obj === obj) // true
console.log(obj === objCopied) // true
Enter fullscreen mode Exit fullscreen mode

두 변수는 이름은 다르지만 같은 객체를 참조하고 있기 때문에 같다.

그래서 둘 중 하나만 변경해도 둘다 바뀌는 모습을 볼 수 있다.

objCopied.greeting = "안녕하세요"

console.log(objCopied) // { jobTitle: "개발자", greeting: "안녕하세요" }
console.log(obj) // { jobTitle: "개발자", greeting: "안녕하세요" }
Enter fullscreen mode Exit fullscreen mode

둘 다 바뀌는게 아니라 같은 객체를 참조한다는 표현이 맞겠다.


함수 매개변수에서 사용하는 값

함수를 사용하면 딱봐도 복잡해진다.

let myVarType = "let"

function myFunction (myVarType) {
  console.log(myVarType) // let
  myVarType = "const"

  return myVarType
}

let result = myFunction(myVarType)
console.log(myVarType) // let
console.log(result) // const
Enter fullscreen mode Exit fullscreen mode

매개변수로 원시 값인 문자열을 전달했는데, 뭐.. 문법을 배웠다면 이렇게 쓰기 시작한다.

문제는 자바스크립트에 슬슬 익숙해져 객체를 활용하기 시작하는데 아래와 같이 매개변수로 객체를 전달할 때 발생한다.

let me = { jobTitle = "개발자" }

function turnOver (me) {
  me.jobTitle = "디자이너"
  return me
}

let result = turnOver(me)
console.log(me) // { jobTitle: "디자이너" }
console.log(result)  // { jobTitle: "디자이너" }
console.log(me === result) // true
Enter fullscreen mode Exit fullscreen mode

직관적으론 아까처럼 달라야 하는데, 같은 객체다. 매개변수의 모양때문에 해깔린다면 다음처럼 바꿀 수 있다.

//function turnOver() {
  let me2 = me
  me2.jobTitle = "디자이너"
//  return me2
//}
// function을 주석처리한건 혹시나 스코프를 설명해야할지도 몰라서...
console.log(me === me2) // true
Enter fullscreen mode Exit fullscreen mode

그럼 이전 챕터에서 다루었던 객체의 예제와 모양이 같아졌다. 매개변수도 결국 일반적인 변수선언과 똑같기 때문에 다르게 생각할 필요 없다.

★ 누군가는 이 현상을 콜 바이 레퍼런스call by reference 혹은 주소 값을 전달한다고 표현하는데 자바스크립트는 포인터 기능이 없다.

만약 함수에서 다른 객체를 반환하고 싶다면 새로운 객체를 만들면 된다.

function turnOver(me) {
  const newProfile = { ...me }
  console.log(newProfile) // { jobTitle: "개발자" }
  console.log(newProfile === me) // false
  newProfile.jobTitle = "디자이너"

  console.log(me) // { jobTitle: "개발자" }
  console.log(newProfile) // { jobTitle: "디자이너" }
  return newProfile
}
Enter fullscreen mode Exit fullscreen mode



객체의 프로퍼티에서도 똑같이 적용된다

객체 사용도 익숙해지면 객체 프로퍼티에 객체를 활용한다.

const company = {
  name: "폭스바겐",
  // 자바스크립트에선 배열도 객체이기 때문에 이 예제에선 객체라고 칭하겠다.
  subsidiary: [
    "벤틀리",
    "부가티",
    "아우디"
  ]
}
Enter fullscreen mode Exit fullscreen mode

위와 같은 객체가 있을 때 프로퍼티를 변수에 담아 활용해보았다.

let name = company.name
name = "폭스바겐 그룹"
console.log(name) // 폭스바겐 그룹
console.log(company) // {name: "폭스바겐", subsidiary: ["벤틀리", "부가티", "아우디"]

const subsidiary = company.subsidiary
subsidiary.push('람보르기니')
console.log(subsidiary === company.subsidiary) // true
console.log(company) // {name: "폭스바겐", subsidiary: ["벤틀리", "부가티", "아우디", "람보르기니"]}

company.subsidiary.push("포르쉐");
console.log(subsidiary) // ["벤틀리", "부가티", "아우디", "람보르기니", "포르쉐"]
Enter fullscreen mode Exit fullscreen mode

변수에서 객체의 프로퍼티로 모양이 바뀌었지만 다르게 생각할 필요가 없다.


리액트에서 상태를 객체로 사용할 때

리액트가 등장하면서 UI를 구현하기 얼마나 편해졌는지 모른다. 그러다보니 이런 저런 데이터를 활용하기 시작하는데 객체를 사용할 때 문제가 발생한다.

import React from 'react'

export default function () {
  const [me, setMe] = React.useState({ jobTitle: "개발자" })
  return (
    <button
      onClick={function () {
        me.jobTitle = "디자이너"
        console.log(me) // { jobTitle: "디자이너" }
        setMe(setMe)
      }}
    >
      직종변경하기(현재 {me})
    </button>
  )
}
Enter fullscreen mode Exit fullscreen mode

버튼에는 직종변경하기 (현재 개발자)가 표시되어있는데 클릭하면 직종변경하기 (현재 디자이너)가 될거라 예상하지만 바뀌지 않는다. 분명 객체의 내용은 바뀌었는데 state의 기준에선 이전 값과 현재 값을 비교했을 때 같기 때문이다.

계속 봐왔듯 객체의 프로퍼티를 바꾼다고 다르게 인식하지 않는다. 새로운 객체를 만들어야 다르다고 인식한다.

setMe({ ...me })
Enter fullscreen mode Exit fullscreen mode

사실 이 예제는 적절하진 않다. 다만 redux나 context를 활용할 때 똑같은 원리가 적용되므로 상대적으로 표기하기 간편한 state를 사용했다.


그런데 null은 object라는데요?

값이 없다는 의미로 null을 사용한다. 유효성 검사를 위해 타입을 확인하는데,

console.log(typeof null) // object
Enter fullscreen mode Exit fullscreen mode

타입이 객체로 나온다. 정말 혼란스럽다.

왜 이렇게 된건지는 해당 포스팅을 참조하면 될 것 같다.


계기

주니어 시절 정말 답답했다. 객체가 내 생각대로 반환되지 않았기 때문이다. 지금도 종종 실수가 있긴하다. 함수 서너개만 거치면 어디서 문제가 생겼는지, 어떻게 바꿀까 고민하는데 생각보다 많은 시간이 든다. 그래서 왠만하면 원시 값을 사용하곤한다.

제이쿼리를 이용해 간단한 돔DOM 조작할 땐 몰라도 문제 없었는데, 앱의 모양에 가까워 질수록 이런 기본이 정말 중요했다.

Top comments (0)