이 글은 https://medium.com/javascript-in-plain-english/i-created-the-exact-same-app-in-react-and-vue-here-are-the-differences-e9a1ae8077fd의
번역글입니다. 오타 및 오역 제보 기쁘게 받도록 하겠습니다.
Vue를 직장에서 사용하기에 Vue를 잘 알고 있다고 생각한다. 어쨌든 나는 울타리 너머에 있는 잔디를 궁금해했다.(잔디는 이 이야기에서 React이다.)
난 React 문서를 읽고 튜토리얼 동영상을 보았는데 모두 좋았지만, 내가 정말로 알고 싶었던 것은 React와 Vue가 어떻게 다른지였습니다.
“다른점”이란 것은 가상돔(Virtual DOMS)이나 페이지가 어떻게 렌더링되는지를 의미하지 않습니다. 나는 코드를 설명해주고 무슨일이 일어나는지 알려주는 것을 원했습니다! 이 차이점을 설명해주는 기사를 찾아서 Vue와 React를 모르는 사람(또는 웹 개발을)들이 Vue와 React의 차이를 더 잘 이해할 수 있기를 원했습니다.
하지만 이를 설명해주는 것을 찾지 못했습니다. 그래서 난 앞으로 나아가서 유사점과 차이점을 보기 위해 직접 만들어보기로 하였습니다. 이 과정에서 관련된 것을 모두 문서화함으로써 기사가 존재하게 될것이라고 생각했습니다.
여기서는 사용자가 ToDo 목록에서 각 ToDo들을 추가하고 삭제해주는 상당히 표준적인 ToDo App을 만들기로 결정했다.
두 앱(Vue, React)은 기본 CLI를 사용했다.(React는 create-react-app, Vue는 vue-cli)
참고로 CLI는 Command Line Interface의 약자입니다.
어쩌다보니 인트로가 생각보다 길어지네요 ㅎㅎ. 두앱의 모양을 빠르게 비교해 보는 것으로 시작하겠습니다!
두 앱의 CSS 코드는 완전히 똑같지만 코드가 위치한 곳이 다릅니다.
그걸 염두에 두고 두 앱의 파일 구조를 보러가봅시다.
두 앱의 구조를 보면 거의 동일한 것을 알 수 있습니다. 유일한 차이점은 React App은 3개의 CSS 파일이 있으나 Vue App은 전혀 없습니다. 그 이유는 create-react-app에서 React 컴포넌트는 style을 유지해주는 파일을 두기로 했지만 Vue CLI는 감싸는 접근법을 채택했습니다. 감싼다는 것은 style들이 실제 컴포넌트 파일 안에 정의된 것을 의미합니다.
참고로 React에서도 라이브러리를 사용하면 동일하게 작성할 수 있습니다.
최종적으로는 두 앱은 같은 일을 하고 React나 Vue에서 CSS를 다르게 구성할 수 없다고 할 수는 없습니다. 파일 구성은 정말로 개인취향입니다. (당신은 CSS 구조화하는 방법을 dev 커뮤니티에서 충분히 많은 토론을 듣게 될 것입니다. 지금은 두 CLI에서 만들어준 구조를 따르도록 하겠습니다.)
여기서 더 진행하기전에 전형적인 Vue와 React 컴포넌트의 모습을 살펴보겠습니다.
이제 전부다 제쳐놓고 이 글의 핵심을 보러 가십시다!
어떻게 data를 바꿀까?
알아보기전에 먼저, “mutate data(data 변이)”의 의미가 무엇이죠? 조금 기술적인 것 같지않아요? mutate data는 기본적으로 우리가 저장시킨 데이터를 바꾸는 것을 의미한다. 만약 우리가 John에서 Mark로 사람 이름의 값을 바꾸고 싶다면, 우리는 ‘mutating the data(데이터 변이하기)’를 하는 것이다. 이것이 React와 Vue 사이의 중요한 차이점입니다.
Vue는 반드시 데이터 객체를 생성한 후에 data를 자유롭게 업데이트할 수 있고, React는 state 객체를 만들고 업데이트를 하려면 조금 더 많은 작업이 필요합니다.
React는 더 좋아지도록 별도의 작업(state를 조작하기 위한 작업)을 수행합니다. 여기서 우리는 조금 더 깊숙히 파고들어가 볼겁니다.
하지만 그 전에 Vue의 data 객체와 React의 state 객체를 잠깐 보도록 합시다.
위 그림을 보면 당신은 양쪽에 같은 데이터가 전달되었다는 것을 알 수 있지만 단순히 다른 이름이 붙어있습니다. 초기화한 데이터를 우리의 컴포넌트에 전달하는 것은 매우, 매우 유사합니다. 하지만 언급했듯이 우리가 두 프레임워크에서 이 데이터를 변경하는 방법은 다릅니다.
우리가 ‘name: ‘Sunil’.’ 라는 데이터가 있다고 가정해봅시다.
Vue에서는 this.name으로 값을 호출합니다. this.name = ‘John’을 호출하여 값을 변경시킬 수 있습니다. 이렇게 내 이름은 존이 되었습니다. 내가 존이라고 불리게 되줄은 몰랐는데 하지만 존이 되버렸네!? ㅎ
React에서는 this.state.name을 호출해서 Vue에서처럼 같은 값을 참조할 것입니다. 여기서 중요한 점은!
우리가 this.state.name = ‘John’ 으로 간단하게 값을 업데이트할 수 없다는 것인데, React는 쉽게 값을 바꿀 수 없게 제약이 걸려있습니다.
도우미 없이 값을 바꾼다니..
어쨌든, 리액트에서는 this.setState({name: ‘John’}) 을 작성합니다. this.setState가 React의 state 변경을 돕는 도우미입니다.
Vue에서랑 한 일과 같은 작업을 했지만, 추가로 작성할 것이 있었습니다.
Vue에서는 data를 업데이트할때마다 setState를 알아서 결합해줍니다.
즉, React에서는 setState와 내부에 업데이트된 데이터를 필요로하지만, Vue는 data 객체의 내부 값을 업데이트하려는 경우 이 작업을 알아서 수행한다.
그러면 React에서는 왜 setState를 필요로할까? 설명은 Revanth Kumar가 하고 있습니다.
“그 이유는 React에서는 특정 라이프 사이클 훅, componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, render, componentDidUpdate 가 state를 변경할 때마다 다시 실행하려하기 때문입니다. setState 함수를 호출하면 변경된 state를 알 수 있다. 만약 state를 직접적으로(위에서 설명한 Vue처럼) 바꾼다면, React에서는 변경을 감지를 유지하고 라이프사이클 훅 실행 등을 위해 더 많은 작업을 해야합니다. 그래서 간단하게 하려고 setState를 사용합니다.(즉, 권장 사항)”
이제 mutation을 알아보았습니다. 우리의 ToDo App에 새 할 일을 추가하는 것을 보러갑시다.
어떻게 새 할 일 목록(new todolist)을 생성합니까?
React:
createNewToDoItem = () => {
this.setState( ({ list, todo }) => ({
list: [
…list,
{
todo
}
],
todo: ‘’
})
);
};
어떤 일이 일어났습니까?
React에서 input 필드에 value라는 속성이 있습니다. value는 양방향 바인딩(two-way binding)과 유사한 함수를 만들어 사용하여 자동적으로 업데이트됩니다.(만약 양방향 바인딩에 대해 들어본적이 없다면, 다음 섹션인 Vue에서 자세히 설명합니다)
양방향 바인딩을 위해 onChange 이벤트 리스너를 추가하여 input 필드와 연결시킵니다. input 필드에서 무슨 일이 일어나는지 알아보기 위해 빠르게 보러가십시다.
<input type=”text” value={this.state.todo} onChange={this.handleInput} />
handleInput 함수는 input 필드의 값이 변경될때마다 실행됩니다. input 필드의 값으로 state 객체 안에 있는 todo을 업데이트합니다.
아래는 handleInput 함수의 코드입니다.
handleInput = e => {
this.setState({
todo: e.target.value
});
};
이제 사용자가 페이지의 + 버튼을 눌러 새 항목을 추가 할 때마다 createNewToDoItem 함수에서 this.setState에 값을 전달하고 실행합니다.
이 함수는 두 매개변수를 갖는데, 첫번째는 state 객체에서 list 배열이고, 두번째는 todo이다.(handleInput 함수에 의해 업데이트된다). 이 함수는 이전의 전체 list를 포함하는 새 객체를 반환하고 그 끝에 todo를 추가합니다. 전체 list는 스프레드 오퍼레이터를 사용하여 추가합니다.(…연산자: ES6 문법. 본적이 없다면 구글링을..)
마지막으로 우리는 빈 문자열 todo를 만들었습니다. input 필드의 값이 자동으로 업데이트됩니다.
Vue:
createNewToDoItem() {
this.list.push(
{
'todo': this.todo
}
);
this.todo = '';
}
Vue에서는 어떤 일이 일어났나요?
Vue에서는 input 필드에서 v-model을 호출하여 handle(조작)합니다. 우리는 v-model로 양방향 바인딩을 할 수 있습니다. 그럼 우리의 input 필드를 보러가서 더 자세히 설명을 해보도록하지요.
<input type=”text” v-model=”todo” />
v-model은 필드에 입력된 키가 toDoItem의 data 객체와 연결됩니다. 페이지가 로드될 때 toDoItem이 빈스트링. 즉, todo: ‘’로 세팅됩니다. 만약 데이터가 있다면(ex: todo: ‘add some text here’), input 필드 안에는 이미 세팅된 값이 로드될 것입니다.
어쨌든, 빈 문자열로 돌아가서 input 필드 안의 텍스트가 todo의 값으로 바인딩됩니다. 이것은 효율적인 양방향 바인딩입니다.(input 필드는 data 객체를 업데이트 할 수 있고 data 객체는 input 필드를 업데이트 할 수 있다.)
createNewToDoItem()로 돌아가 코드를 살펴보면 list 배열에 todo를 push 하고 빈 문자열인 todo를 업데이트하는 것을 볼 수 있습니다.
List 삭제는 어떻게 합니까?
React:
deleteItem = indexToDelete => {
this.setState(({ list }) => ({
list: list.filter((toDo, index) => index !== indexToDelete)
}));
};
리액트에서 어떤 일이 일어났나?
deleteItem 함수는 ToDo.js안에 있고, TodoItem.js안에서 가장 먼저 참조 할 수 있었습니다. <ToDoItem />의 prop(내려받은 값)으로 deleteItem() 함수를 다음과 같이 전달합니다.
<ToDoItem deleteItem={this.deleteItem.bind(this, key)}/>
먼저 함수를 ToDoItem에 전달하여 자식(ToDoItem)이 접근 할 수 있게 합니다. ToDoItem이 클릭할때 삭제하려는 시도하는 기능을 구분하기 위해 key 매개변수를 전달할 뿐만아니라 바인딩하는 것을 볼 수 있습니다. 그런 다음 ToDoItem 컴포넌트 안에서 다음을 합니다.
<div className=”ToDoItem-Delete” onClick={this.props.deleteItem}>-</div>
부모 컴포넌트 안에 있는 함수를 참조하기 위해 this.props.deleteItem을 참조했습니다.
Vue:
onDeleteItem(todo){
this.list = this.list.filter(item => item !== todo);
}
뷰에서 어떤 일이 일어났나?
뷰에서는 조금 다른 접근 방법이 필요합니다. 우리는 기본적으로 세가지를 해야합니다.
가장 먼저, 요소에서 함수를 호출해야한다:
<div class=”ToDoItem-Delete” @click=”deleteItem(todo)”>-</div>
그 후 자식 컴포넌트(이 경우 ToDoItem.vue)의 내부의 메소드(함수)로 emit 함수를 만들어야한다. 이 함수는 다음과 같다.
deleteItem(todo) {
this.$emit('delete', todo)
}
ToDo.vue의 안에 ToDoItem.vue를 추가할 때 실제로 함수를 참조한다는 것을 알 수 있습니다.
<ToDoItem v-for="todo in list"
:todo="todo"
@delete="onDeleteItem" // <-- this :)
:key="todo.id" />
이것이 사용자 정의 이벤트 리스너입니다. emit이 ‘delete’문자열로 트리거되는 모든 상황을 감시합니다. 만약 onDeleteItem이라는 함수가 트리거됩니다. 이 함수는 ToDoItem.vue이 아니라 ToDo.vue 안에 있습니다.
이 함수는 클릭된 항목을 제거하기 위해 더 쉽고 간단히 필터링하여 data 객체 안에 있는 todo 배열을 제거합니다.
또한 Vue 예제에서 @click 리스너 내부에 $emit을 작성하는 방법도 있다.
<div class=”ToDoItem-Delete” @click=”this.$emit(‘delete’, todo)”>-</div>
이 방법은 3 단계에서 2 단계로 작업을 줄였습니다. 개인취향에 따라 작성이 달라집니다.
간단히 말해서, React의 자식 컴포넌트는 this.props를 통해 부모 함수에 접근할 수 있습니다.(프로시저를 전달하면 상당히 표준적인 방법이므로 다른 React 예제에서는 많이 보게 될겁니다) 반면에 Vue는 부모 컴포넌트 내부에서 자식에게 event를 내보내야합니다.
이벤트 리스너는 어떻게 전달합니까?
React:
이벤트 리스너는 클릭 이벤트와 같이 간단한 것은 직관적이라 단순하다. 여기 새로운 ToDo 아이템을 만드는 버튼에 생성된 클릭 이벤트의 예제가 있다.
<div className=”ToDo-Add” onClick={this.createNewToDoItem}>+</div>
vanilla JS로 인라인 onClick을 처리하는 방법처럼 엄청 쉽고 예뻐 보인다. Vue에서 말했듯이 엔터 버튼을 누를 때마다 이벤트 리스너를 설정하는 데 약간 시간이 걸렸습니다. 이것은 필수적으로 onKeyPress 이벤트가 input 태그에 의해 처리되도록 했습니다.
<input type=”text” onKeyPress={this.handleKeyPress}/>.
이 함수는 다음과 같이 ‘enter’키를 눌렀을 때마다 createNewToDoItem 함수를 실행합니다.
handleKeyPress = (e) => {
if (e.key === 'Enter') {
this.createNewToDoItem();
}
};
Vue:
뷰에서는 명확합니다. 우리는 단순하게 @ 기호를 사용하고 하기를 원하는 이벤트 리스너의 타입을 사용하면 됩니다. 예를 들어 클릭 이벤트 리스너를 추가하기 위해 이렇게 작성할 수 있습니다.
<div class="ToDo-Add" @click="createNewToDoItem()">+</div>
주석: @click은 실제로 v-on:click의 약자입니다. Vue 이벤트 리스너의 멋진 것은 체인형태로 묶을 수있는 것들이 많다는 것입니다.
.once와 같이 이벤트 리스너가 한번만 실행되도록 합니다. 키를 처리하기 위한 특정 이벤트 리스너들을 작성할 때 많은 단축키가 있습니다. 엔터 버튼을 누를때마다 새로운 ToDo 항목을 만들기 위해 React에서 이벤트 리스너를 만드는 것이 꽤 오래 걸리는 것으로 나타났습니다. 뷰에서는 아래처럼 간단히 쓸 수 있었다.
<input type=”text” v-on:keyup.enter=”createNewToDoItem” />
or
<input type=”text” @:keyup.enter=”createNewToDoItem” />
어떻게 자식 컴포넌트로 데이터를 전달합니까?
React:
리액트에서 우리는 props로 자식 컴포넌트에게 생성된 것을 전달했습니다. 다음과 같이:
<ToDoItem key={key} item={todo} />
여기서 두 개의 props가 ToDoItem 컴포넌트에 전달됩니다. 이제부터는 this.props를 통해 자식 컴포넌트에서 참조할 수 있습니다. 따라서 item.todo prop에 접근하려면 우리는 간단하게 this.props.todo를 호출하면 됩니다.
Vue:
뷰에서는 자식 컴포넌트를 생성한 부근에서 props를 전달합니다. 다음과 같이:
<ToDoItem v-for=”todo in list”
:todo=”todo”
:key=”todo.id”
@delete=”onDeleteItem” />
이것이 끝나면 자식 컴포넌트의 props 배열에 props: [‘id’, ‘todo’]와 같이 전달합니다. 그들의 이름은 자식에서 ‘id’와 ‘todo’라는 이름으로 참조할 수 있습니다.
부모 컴포넌트로 데이터를 다시 보내려면 어떻게 해야합니까?
React:
가장 먼저 자식 컴포넌트를 호출하는 곳에서 자식 props를 참조해서 자식 컴포넌트에 함수를 전달합니다. this.props.whateverTheFunctionIsCalled를 참조해서 onClick과 같은 것으로 자식에게 함수를 추가합니다. 이 함수는 부모 컴포넌트에서 함수가 실행됩니다. 우리는 ‘List 삭제는 어떻게 하죠?’에서 전체 프로세스의 예제를 들어볼 수 있다.
Vue:
우리의 자식 컴포넌트에서 단순히 부모 함수에 다시 값을 내보내는 함수를 작성합니다. 부모 컴포넌트에서 우리는 값이 언제 내보내지는지를 기다리는 함수를 작성하면 함수를 호출할 수 있다. 이 전체 프로세스의 예제는 ‘List 삭제는 어떻게 하죠?’에서 볼 수 있습니다.
그리고 드디어 다 했다!
데이터를 추가, 제거하고 변경하고 부모에서 자식에게로 props로 데이터를 전달하고 이벤트 리스너로 자식에서 부모에게 데이터를 보내는 방법을 살펴보았습니다. 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