[JavaScript] 토이프로젝트로 todolist를 구현하는데 인풋창에 '[object Object]' 값이 들어온다

선영·2022년 12월 1일
0

JavaScript

목록 보기
24/27
post-thumbnail

🧨 문제


개발자도구에 찍힌 console.log(title, content)값은 총체적 난국이었다. title엔 객체가 들어오고, content는 undefined값이 찍힌다.

문제가 발생한 코드는 아래와 같다.
(뎁스구조를 명시하고, import, export는 생략되었다)

// 자식컴포넌트 ToDoForm
function ToDoForm({
  title, content,
  onClickAddTask, onChangeContents,
}: ToDoFormProps) {
  return (
    <form className='to-do-form'>
      <div>
        <label htmlFor="title">제목</label>
        <input
          type="text"
          id="title"
          onChange={onChangeContents}
          value={title}
          name="title"
        />
        <label htmlFor="content">내용</label>
        <input
          type="text"
          id="content"
          onChange={onChangeContents}
          value={content}
          name="content"
        />
      </div>
      <div>
        <button
          type="button"
          onClick={() => onClickAddTask()}
        >
          추가하기
        </button>
      </div>
    </form >
  );
}

// 부모컴포넌트 App
function App() {
  const [contents, setContents] = useState<Contents>({ 
    tasks: [{
      id: 1,
      title: '',
      content: '',
      isDone: false,
    }],
  });

  const {
    tasks, tasks: [title, content]
  } = contents;
  const newTask = tasks[tasks.length - 1];

  const handleChangeContents = (event: React.ChangeEvent<HTMLInputElement>) => {
    const { target: { name, value } } = event;

    setContents({
      ...contents,
      tasks: [{
        ...newTask,
        [name]: value,
      }],
    })
  }

  function handleClickAddTask() {
    const newId = newTask.id + 1;

    setContents({
      ...contents,
      tasks: [...tasks,
      {
        id: newId,
        title: '',
        content: '',
        isDone: false,
      }
      ],
    })
  }

  return (
    <>
      <TopNavBar />
      <ToDoForm
        title={title}
        content={content}
        onClickAddTask={handleClickAddTask}
        onChangeContents={handleChangeContents}
      />
    </>
  );
}

🤔 원인


프로토타입 체이닝

원인을 알기 위해 일단 '[object Object]'가 무엇인지 알아야할 것 같았다. 찾다보니 이번주에 공부하고 있는 주제인 프로토타입과 관련이 있었다.

Object.prototype.toString은 인자로 넘겨받은 object의 클래스 타입을 반환하는 메소드다.
자바스크립트는 객체지향 프로그래밍이다. 즉, 원시타입을 제외한 나머지 값들은 모두 객체다. 그리고 js의 타입들 중 null과 undefined를 제외한 모든 타입들은 Object의 인스턴스들이다.

즉, [[prototype]]프로퍼티를 가지고 있어 프로토타입 체이닝을 통해 Object.prototype까지 도달할 수 있다는 뜻이다. 실제로 Number instanceof Object를 콘솔창에 찍어보면 true를 반환하는 것을 확인할 수 있다.

function A(a) {
    this.a = a;
    this.toString = function() {
        return this.a;
    }
}

const NewA = new A('sunyoung');

NewA.toString(); // NewA인스턴스는 A생성자함수의 메서드 toString을 사용할 수 있다.

// 'sunyoung'

js의 모든 객체는 자신만의 메소드를 가질 수 있다. 위 예제의 프로세스는 아래와 같다. 아래의 반복작업은 toString을 찾을때 까지 혹은 [[prototype]]프로퍼티에 연결된 객체가 null일 때 까지 반복한다.

1/ toString을 호출했을 때 자바스크립트 엔진은 NewA가 자신의 스코프에서 toString을 갖고있는지 확인한다.

2-1/ 만약 갖고 있으면, 해당 스코프의 toString이 호출된다.
2-2/ 만약 갖고 있지 않다면, NewA[[prototype]]프로퍼티(즉, __proto__접근자 프로퍼티)에 연결된 상위객체로 들어가 그 스코프에 toString이 있는지 확인한다.

3-1/ 만약 갖고 있으면 해당 스코프 내의 toString이 실행된다.
3-2/ 갖고 있지 않다면 같은 작업을 반복한다.

call, apply

다시 본론으로 돌아와, Object.prototype.toString은 인자로 넘겨받은 object의 클래스 타입을 반환하는 메소드다. Object.prototype.toString.apply({})을 콘솔창에 출력하면 '[object Object]'이 출력된다.

여기서 call, apply는 아래에서 볼 수 있듯이, [1,2,3]인자를 전달했을 때 this가 그 배열을 참조할 수 있게 해준다. 아무 인자도 전달하지 않으면 thisWindow전역객체를 참조한다.

function print() {
  console.log(this);
}
print(); // Window
print.call([1, 2, 3]); // [1, 2, 3]
print.apply([1, 2, 3]); // [1, 2, 3]

toString의 동작방식은 ECMAScript 명세서에 정의되어 있다. 아래와 같이 해당 명세서에 따르면 Object.prototype.toSting은 인자로 객체를 전달 받았기 때문에, '[object Object]'을 전달받은게 된다.

🙌 해결과정


다시 처음으로 돌아가보자. 내가 처음에 한 말을 인용하자면,

console.log(title, content)`값은 총체적 난국이었다. 
title엔 객체가 들어오고, content는 undefined값이 찍힌다.

typeof title은 object로 콘솔창에 출력된다. 결국 title에 객체값이 들어오기 때문에 인풋창에 '[object Object]'가 찍히는거였다.

아래 코드에서 내가 구조분해할당을 잘못하고 있어서 title에 객체가 들어오는거였다.

const [contents, setContents] = useState<Contents>({
    tasks: [{
      id: 1,
      title: '',
      content: '',
      isDone: false,
    }],
  });

  const {
    tasks, tasks: [title, content]
  } = contents;

그래서 코드를 아래와 같이 고쳐줘서 해결할 수 있었다.

const [contents, setContents] = useState<Contents>({
    tasks: [{
      id: 1,
      title: '',
      content: '',
      isDone: false,
    }],
  });

  const {
    tasks, tasks: [title, content]
  } = contents;

😎 결론


문제는 해결했지만 왜 input태그의 value값에 객체가 들어오니까 '[object Object]'로 반환되는걸까? How to Display JavaScript Objects? 페이지를 보면, 아래와 같이 doc에서 id값이 'demo'인 요소를 찾아서 innerHTML값을 person(객체)로 할당하여 DOM을 조작하려한다. 그러면 내가 의문을 갖는 것과 동일하게 '[object Object]'가 p태그안에 들어가는 것을 볼 수 있다. 그러나 객체를 배열로 변환하거나 프로퍼티값을 직접 넣어주면 p태그안에 값들이 잘 출력된다. 왜 그러는걸까? 이는 innerHTML이 동작하는 방식과 관련이 있다.

<!DOCTYPE html>
<html>
<body>

<h2>JavaScript Objects</h2>
<p>Displaying a JavaScript object will output [object Object]:</p>

<p id="demo"></p>

<script>
const person = {
  name: "John",
  age: 30,
  city: "New York"
};

document.getElementById("demo").innerHTML = person;

</script>

</body>
</html>

Element.innerHTML 문서를 보면, 아래와 같이 나와있다. 특히, 문자열 htmlString innerHTML을 파싱(구문 분석)하여 노드로 대체한다고 나와있는 부분을 보자.

즉, 아래와 같은 형태가 된다. 결국 innerHTML은 주어진 값을 문자열로 toString으로 변환하는 것이고, Object가 기본으로 toString[object Object]이기 때문에 그렇게 나온 것을 확인할 수 있었다.

['<div>Hello</div>'].toString() // '<div>Hello</div>'
({value: '<div>Hello</div>'}).toString() // // '[object Object]'

☑️ 참고


profile
Superduper-India

0개의 댓글