(번역) React, Vue로 동일한 앱 만들기


React와 Vue 관련하여 성능, 특징 등 비교하는 글을 많이 봤는데, 코드가 어떻게 다른지 비교하는 글이 있어 번역해 보았습니다.

회사에서 Vue로 작업을 하기 때문에, Vue를 꽤 잘 다룬다고 생각합니다. 하지만, 저는 UI 라이브러리의 큰 축인 React에 대해서도 궁금해지기 시작했습니다.

저는 React 문서를 읽고 튜토리얼 비디오도 보았습니다. React가 좋아 보였지만, 저는 실제 React와 Vue가 어떠한 차이점이 있는지 알고 싶었습니다. 저는 두 도구가 Virtual DOM을 사용하거나, 어떻게 렌더를 하는지에 대한 것보다, 누군가 두 툴의 코드에 대해 설명을 해줬으면 했습니다. 그래서 Vue, React(또는 웹개발)을 처음 접하는 사람들이 이해를 좀 더 쉽게 할 수 있으면 좋겠다고 생각했습니다.

하지만, 그러한 글을 찾지 못했고, 이 기회에 제가 직접 유사점과 차이점을 알아봐야겠다고 생각했습니다. 이러한 과정을 문서화 했고, 이렇게 포스팅을 하게 되었습니다.

저는 유저가 리스트를 생성하고 삭제할 수 있는 일반적인 To-do 앱을 만들기로 했습니다. 두 앱 모두 default로 제공되는 CLI (Command Line Interface)를 사용해서 만들었습니다.

인트로가 꽤 길어졌습니다. 두 앱이 어떻게 다른지 빨리 살펴보도록 하겠습니다.

두 앱의 CSS 코드는 완전히 동일합니다. 하지만 이 코드가 위치한 곳은 다릅니다. 두 앱의 파일 구조를 아래와 같이 살펴보도록 하겠습니다. 두 앱의 파일 구조가 매우 유사한 것을 알 수 있습니다. 유일한 차이점은 React는 css 파일이 별도고 있고, vue의 경우 그렇지 않다는 것입니다. 이는 Create-react-app의 컴포넌트는 컴포넌트에서 사용하는 style 파일을 별도로 가지고 있고, vue는 컴포넌트 내부에서 스타일을 정의하기 때문입니다.

궁극적으로 화면에 보이는 결과는 같습니다. 그리고 React와 Vue 두 앱에서 위에 나타난 것과 다른 css 구조를 만들 수 있습니다. 웹상에서도 이에 대한 토론이 많지만, 우선 이 글에서는 CLI에 따른 구조로 글을 쓰도록 하겠습니다.

우선, React와 Vue의 일반적인 컴포넌트 형태에 대해서 아래와 같이 살펴보도록 하겠습니다.

How do we mutate data?

먼저, 데이터를 조작(mutate)한다는 것이 무슨 의미를 가질까요? 이것은 우리가 저장한 데이터를 변화시키는 것을 말합니다. 예를 들어 어떠한 사람의 이름을 John에서 Mark로 변경한다면 우리는 데이터를 조작한다고 합니다. 그리고 이것이 Vue와 React의 주요 차이점이 될 수 있습니다. Vue의 경우 Data Object를 생성하고 자유롭게 업데이트를 할 수 있습니다. React는 state를 만들어 데이터를 저장하고, 데이터를 수정하기 위해서는 일부 추가 작업이 필요합니다. 이 추가 작업은 좋은 의도를 지니고 있습니다. 우선 두 앱의 데이터 저장 방법에 대해 살펴보겠습니다.

위와 같이 데이터를 컴포너틑에 추가하는 방법은 유사합니다. 하지만, 앞서 언급했듯이 이 데이터를 변경하는 방법은 다릅니다.

우리가 'Sunil'이라는 name의 데이터 생성했다고 가정해 봅시다. Vue에서 우리는 "this.name"으로 데이터에 접근하고, "this.name = 'John'"으로 수정할 수 있습니다.

React에서 "this.state.name"으로 접근할 수 있습니다. 그리고 React에서 state에 저장된 데이터를 변경하려면 setState 함수를 이용해 "this.setState({ name: 'John' })"으로 변경할 수 있습니다.

React에서 이처럼 데이터 변경을 위해 추가 작업이 필요합니다. Vue의 경우 데이터를 업데이트하면 내부적으로 setState를 대신해주기 때문에 이러한 추가 작업이 필요 없이 같은 결과를 가져올 수 있습니다. 그러면 React는 왜 데이터 변경을 위해 setState가 필요할까요? 이에 대한 대답은 (Revanth Kumar)[https://medium.com/@revanth0212]의 설명을 빌려오겠습니다.

React에서는 state가 변경되면, componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, render, componentDidUpdate와 같은 일부 라이프 사이클 메소드를 실행하기 때문입니다. setState가 호출되면 리액트는 state가 변경되었다는 것을 알 수 있습니다. 만약 직접 state를 변경하면, 변화를 감지하고 어떠한 라이프 사이클 훅을 실행할지 결정하기 위해 React는 더 많은 작업을 해야할 것입니다.

다음으로 to-do app에 아이템을 추가하는 방법에 대해 살펴보겠습니다.

How do we create new ToDo Items?

React

createNewToDoItem = () => {
  this.setState(({ list, todo }) => ({
    list: [
      ...list,
      {
        todo
      }
    ],
    todo: ''
  }));
};

How did React do that?

React는 Input 태그의 value 속성을 사용할 수 있고, 몇몇 함수를 이용해 자동으로 업데이트되도록 할 수 있습니다. (이것은 vue의 two-way-binding과 유사합니다. two-way-binding에 대해서는 뒤에서 설명하겠습니다.) 우리는 또한 Input 테그에 onClick 속성과 event listener를 추가했습니다.

<input type="text" value={this.state.todo} onChange={this.handleInput} />

handleInput 함수는 input 태그의 값이 변경될 때마다 호출이 됩니다. 이 함수는 setState를 이용해 state의 값을 input의 value로 변경합니다.

handleInput = e => {
  this.setState({
    todo: e.target.value
  });
};

유저가 new item을 추가하기 위해 플러스 버튼을 클릭할 때마다 createNewToDoItem 함수는 setState를 실행 후 함수를 넘겨줍니다. 이 함수는 두 개의 인자를 받는데, 첫 번째로 state의 list array, 두 번째로 todo를 받습니다. ('todo'는 handleInput 함수에 의해 업데이트됩니다.) 두 개의 인자를 받은 함수는 새로운 객체를 반환하는데, 기존의 list에 'todo'를 추가한 리스트를 포함하고 있습니다.

마지막으로, state의 todo를 ''로 변경하여, input 필드에 값이 없도록 만듭니다.

Vue

createNewToDoItem() {
  this.list.push(
    {
      'todo': this.todo,
    }
  );
  this.todo = '';
}

How did Vue do that?

Vue의 input field는 v-model이라는 속성을 가지고 있습니다. 이것은 two-way binding을 가능하게 합니다. 먼저 input 테그를 살펴봅시다.

<input type="text" v-model="todo" />

위 V-model은 input 테그를 우리가 toDoItem이라 부른 data 객체에 묶어줍니다. 페이지가 로드되면, toDoItem의 todo속성은 빈 string('')이 됩니다. 만약 초깃값이 ''이 아닌 다른 값의 string이라면 해당하는 값이 화면에 출력될 것입니다. 아무튼 어떠한 값을 input tag에 입력하면 이 값은 해당하는 data 객체 'todo'속성을 업데이트합니다. 이것이 two-way binding입니다. input tag는 data 객체를 업데이트하고, data object는 input field를 업데이트합니다.

createNewToDoItem 코드를 살펴보면 list에 새로운 'todo'리스트를 추가하고, todo의 값을 ''로 변경합니다.

How do we delete from the list?

React

deleteItem = indexToDelete => {
  this.setState(({ list })) => ({
    list: list.filter((toDo, index) => index !== indexToDelete)
  }));
}

How did React do that?

deleteItem 함수가 ToDo.js안에 위치해 있기 때문에 ToDoItem 컴포넌트에 prop으로 전달하면, 쉽게 참조할 수 있습니다.

<ToDoItem deleteItem={this.deleteItem.bind(this, key)} />

그리고 ToDoItem 컴포넌트에서 아래와 같이 props를 이용할 수 있습니다.

<div className="ToDoItem-Delete" onClick={this.props.deleteItem} />

부모 컴포넌트에 위치한 함수를 이용하기 위해서 단순히 this.props.deleteItem을 실행했습니다.

Vue

onDeleteItem(todo) {
  this.list = this.list.filter(item => item !== todo);
}

How did Vue do that?

Vue에서는 조금 다른 접근이 필요하면, 다음과 같은 세 가지 작업을 할 수 있습니다.

첫째로, 우리가 Vue에서 함수를 실행하길 원하는 요소를 살펴보겠습니다.

<div class="ToDoItem-delete" @click="deleteItem(todo)"></div>

이후, 우리는 child component(ToDoItem.vue) 안에서 emit function을 만들어야 합니다.

deleteItem(todo) {
  this.$emit('delete', todo);
}

ToDo.vue컴포넌트 안에서 ToDoItem.vue를 추가할 때 우리는 onDeleteItem이라는 함수를 참조하고 있습니다.

<ToDoItem
  v-for="todo in list"
  :todo="todo"
  @delete="onDeleteItem"
  :key="todo.id"
/>

이것은 custon event-listener라고도 알려졌으면, 'delete'라는 문자열과 함께 emit이 trigger 되면 onDeleteItem listener가 작동합니다. onDeleteItem method는 ToDo.vue 컴포넌트 안에 있으며, 위에서 명시된 바와 같이 click 된 아이템을 data object에서 삭제합니다.

추가로, deleteItem 함수를 사용하지 않고 아래와 같이 단축할 수도 있습니다.

<div class="ToDoItem-Delete" @click="$emit('delete', todo)"></div>

요약하면, React는 자식 컴포넌트에서 부모 컴포넌트의 함수에 접근하려면 prop으로 전달받아야 합니다. Vue의 자식 컴포넌트에서 이벤트를 발생시키면 부모 컴포넌트에서 이를 받아 함수를 실행시킬 수 있습니다.

How do we pass event listeners?

React

리액트에서 요소에 event listener를 추가하는 것은 간단합니다.

<button className="ToDo-Add" onClick={this.createNewToDoItem} />

바닐라 자바스크립트로 작성할때와 거의 같습니다. Input Tag에 keypressevent를 추가하는 것은 다음과 같습니다.

<input type="text" onKeyPress={this.handleKeyPress} />

위의 input tag는 엔터키가 눌리면, createNewToDoItem 함수를 실행시킵니다.

handleKeyPress = e => {
  if (e.key === 'Enter') {
    this.createNewToDoItem();
  }
};

Vue

Vue의 경우 간단하고 쉽습니다. 우리는 단순히 @를 사용해 event-type을 정할 수 있습니다. 그리고 event listener를 추가해 줍니다.

<button class="ToDo-Add" @click="createNewToDoItem()"></button>

Note : @click은 v-on:click의 축약형입니다. Vue의 event listener의 장점 중 하나는 chaining이 가능하다는 것입니다. 예를 들어 once와 같은 것이 있습니다. once의 경우 함수를 한 번만 실행 후 다시 실행하지 않습니다. Vue의 경우 event listener를 추가하는 것은 매우 단순합니다. React의 버튼 클릭 시 새로운 아이템을 등록하는 이벤트 리스터를 추가하려면 조금 더 코드가 길 것입니다. Vue의 경우 아래와 같이 진행할 수 있습니다.

<input type="text" v-on:keyup.enter="createNewToDoItem"/>

Hoe do we pass data through to a child component?

React

React에서 우리는 prop을 Child Component에 직접 전달할 수 있습니다.

<ToDoItem key={key} item={todo} />

위의 코드에서 우리는 두 prop을 전달했습니다. 해당 Child Component 내부에서 위의 전달 받은 prop에 접급하려면 단순히 this.props.item으로 실행하면 됩니다.

Vue

Vue에서 prop을 전달하기 위해서 child component가 생성되는 지점에서 전달할 수 있습니다.

<ToDoItem
  v-for="todo in list"
  :todo="todo"
  :key="todo.lid"
  @delete="onDeleteItem"
/>

위 작업이 완료된 후, child component 내에서 props array에 "props: ['todo']와 같이 전달을 해야합니다. 이후 컴포넌트에서 'todo'로 해당 데이터에 접근이 가능합니다.

How do we emit data back to a parent component?

React:

React에서 prop을 통해 함수를 내려줄 수 있습니다. 그리고 이 함수를 이용해 필요한 인자를 전달할 수 있습니다.

Vue:

Vue의 경우 Child component에서 특정 값을 방출하는 함수를 작성할 수 있습니다. Parent component에서 특정 이벤트를 주시하는 함수를 작성해, 해당 이벤트가 발생하면 함수가 작동하도록 할 수 있습니다. 우리는 앞서 "How do we delete from the list" 섹션에서 이 과정을 살펴봤습니다.

우리는 React와 Vue에서 데이터를 추가, 삭제, 변경, 전달하는 방법에 있어 코드가 어떻게 다른지 살펴봤습니다. 물론 React와 Vue의 다른 차이점들은 매우 많습니다. 하지만, 이 글이 React, Vue의 기반을 이해할 수 있는데 도움이 되었으면 합니다.

Github links to both apps:

Vue ToDo: https://github.com/sunil-sandhu/vue-todo

React ToDo: https://github.com/sunil-sandhu/react-todo