이전 글에서 알아봤듯, React와 Vue는 서로 공통점이 많이 있다.
하지만 둘이 완전히 똑같다면 애초에 비교에 의미가 없었을 것이다. 이번에는 React와 Vue가 어떻게 다른지 비교해보자.
우선, React나 Vue와 같은 라이브러리가 없다면 자바스크립트를 통한 HTML 조작을 어떻게 해야 하는지부터 생각해보자.
const button = document.createElement('button')
const text = document.createElement('p')
element.innerText = '눌러줘'
element.addEventListener('click', () => {
text.innerText = '눌렀다!'
})
const root = document.getElementById('root')
root.appendChild(button)
root.appendChild(text)
기본적으로 권장되는 방법이지만, 개발하는 입장에선 아주아주 피곤하다.
뭔가 변화를 줘야하는 함수마다 뭐가 어떻게 바뀌어야 하는지 일일이 다 써줘야 하고, 내가 만든게 실제로 어떤 구조의 HTML으로 나올지 알아보기가 어렵다.
버튼 누르면 텍스트가 나오는 정도의 수준이니깐 코드가 이렇게 짧은거지, 상황에 따라 요소가 추가되거나 제거되어야 한다면, 그리고 그 새로 추가/제거된 요소에서도 이벤트를 사용한다면 일일이 이벤트를 걸어주고 빼줘야한다.
const context = {
text: '',
}
const render = () => {
document.getElementById('root').innerHTML = `
<button>눌러줘</button>
<p>${context.text}</p>
`
}
const handleClick = () => {
context.text = '눌렀다!'
render()
}
render();
정해진 순간에 HTML을 문자열로 통째로 뿌려버리는 방법이다.
아까것보다 보기엔 훨씬 나아보인다. 하지만 문제점들이 생겼다.
우선 이벤트핸들링을 인라인으로 직접 넣어줘야 한다. 그 말은 특정 이벤트를 위해 사용하는 모든 함수들이 전역변수여야 한다는 뜻이기도 하다. 간단한 HTML일땐 별 문제 없겠지만 애플리케이션이 점점 커지다보면 수십개에서 수백개의 함수들이 전역스코프에서 이름이 겹치지 않으면서 만들어져야 한다는 뜻이다.
차라리 아까처럼 DOM에 직접 접근했을땐 이런 문제는 없었다. 그냥 해당 DOM에 onClick 메서드를 추가해주면 되었으니깐.
또한, 값 일부만 바꾸더라도 전체 태그를 지우고 다시 만들어야 한다. 길쭉한 내용물이 있어서 아랫쪽으로 스크롤을 한 다음 태그에 변화사항이 생기도록 만든다면, 변화사항이 생길때마다 스크롤이 맨 위로 이동해버리게 될 것이다. 이것 역시 DOM API로 직접 태그를 변경할때에는 없었을 문제점이다.
그밖에 수많은 문제점들이 계속 발견될 것이다. 코드의 가독성은 좋아졌지만, 사실상 자바스크립트 없이 페이지 이동만으로 화면을 제어하던 2000년대 초반 수준의 웹 경험만 제공 가능해지는 것이다.
React는 JSX라는 자신들만의 특별한 언어인 JSX로 이 문제를 해결한다.
JSX는 템플릿과 유사한 형태를 가진다. 자바스크립트에 HTML구조를 직접 작성하는 것이다.
const render = ({ text, handleClick }) => {
return (
<>
<button onClick={handleClick}>눌러줘</button>
<p>{text}</p>
</>
)
}
하지만 이건 엄밀히 말하면 템플릿이 아니다. React는 이걸 HTML문자열로 치환해서 곧바로 뿌리는게 아니라, 개발자들에겐 가독성이 떨어지지만, 사용성 측면에선 더 효과적인 방법인 DOM으로 바꾼다.
const render = ({ text, handleClick }) => {
return React.createFragment([
React.createElement('button', {onClick: handleClick}, '눌러줘'),
React.createElement('p', null, text)
])
}
주: 이해를 돕기 위한 코드일 뿐이며 자세하게 어떻게 바뀌는지는 필자도 잘 모른다.
리액트에서 JSX는, 개발자에 의해 작성될 때는 템플릿처럼 사용할 수 있으며, 각각의 JSX는 값으로 취급된다. 대충 템플릿에 의해 만들어진 HTML 문자열처럼 취급해도 된다는 것이다.
하지만 JSX는 실제로 브라우저에서 사용될 때 DOM을 생성하는 함수로 적절하게 변환된다. 그 순간의 JSX는 템플릿이 아니다. DOM을 생성하는 함수가 어떻게 실행되어야 할지를 알려주는 명령문으로써 작동하게 되는 것이다.
이 JSX는 실제로는 HTML문법도 자바스크립트 문법도 아니다. 리액트가 개발자의 의도를 파악하고 DOM을 만들고 변경하는 방법을 알려주는 코드일 뿐 정상적인 문법은 아니기 때문에, 브라우저에서 작동되기 위해선 JSX를 DOM 조작용 함수로 치환해주는 별도의 과정이 필요하다.
그 과정을 자동화해둔 환경이 우리가 알고 있는 Create React App과 같은 것들이다.
Vue 역시 마찬가지로, 최종 결과물은 DOM 생성을 하는 함수들의 집합이다. 하지만 그것을 만들어내기 위해서 사용하는 방법은 React와 조금 다르다.
React는 JSX를 브라우저에서 작동 가능한 코드로 변환해주는 작업환경이 필수적이지만, Vue는 이미 존재하는 HTML/Javascript 문법을 통해 React가 제공하는 일을 하도록 만들어졌다.
다시말해, Vue는 특별한 개발환경이 없는 상황에서도 사용자가 작성한 템플릿을 문제없이 읽어내고 그걸 DOM API로 변환하는 기능을 가지도록 만들어졌다.
<div id="root">
<button v-on:click="handleClick">눌러줘</button>
<p>{{text}}</p>
</div>
이 HTML 코드는 웹브라우저에서 큰 문제를 일으키지 않는다.
같은 일을 하기 위해 썼던 React의 JSX코드에서, 버튼 부분을 다시한번 보자
<button onClick={handleClick}>눌러줘</button>
버튼에 달려있는 onClick
이라는 속성엔 쌍따옴표 없이 중괄호로 묶여있는 함수가 들어있다.
onClick 이벤트에서 사용될 함수를 전역함수로 만들어서 쓰지 않고 DOM API에 의해 이벤트핸들링 되도록 JSX에서 지원하는 문법이다. html 속성에 해당하는 코드에서 따옴표 대신 중괄호를 쓰면 리액트 내부적으로 그부분을 알아서 처리해 주는 것이다.
하지만 이건 사실 HTML 문법적으로는 잘못되었다.
HTML에서 모든 속성은 쌍따옴표로 묶여있어야 한다. 만약 위 JSX 코드를 브라우저에 HTML코드로 직접 입력한다면, 브라우저는 곧바로 해당 HTML 코드를 자동수정해버려 다음처럼 만들어버린다.
<button onClick="{handleClick}">눌러줘</button>
눌러도 별 일은 일어나지 않는다. HTML 명세에 onclick
이라는 속성은 있어도 onClick
은 없다. 만약 저걸 onclick
으로 바꿔준다면 익명의 객체 리터럴이 되고 handleClick 이라는 전역변수를 찾다가 에러를 낼 것이다.
다시 Vue로 돌아와보자.
<button v-on:click="handleClick">눌러줘</button>
Vue에서는 일반 HTML속성이 아닌 Vue에서 관리하도록 만들 이벤트핸들러를 설정할 때에도 그냥 쌍따옴표를 쓴다. 때문에 속성의 값 부분에서 HTML문법에러가 일어나지 않으며, 브라우저는 이걸 강제로 수정하지 않는다.
속성명인 v-on:click
이 부분은 좀 이상하게 생겼다. 하지만 신기하게도 HTML에서 속성명에 -
나 :
를 사용하는 것은 딱히 문법오류는 아니라고 한다. 때문에 HTML에서 이 코드는 문법오류는 아니다. 그저 브라우저가 모르는 속성명이 사용되었을 뿐.
템플릿 코드가 HTML 문법과 호환성을 가져서 좋을게 뭘까
Vue는 별도의 개발환경이 갖추어지지 않은 상태에서도 사용이 가능하다. React에선 상상할 수도 없는 일이다.
<html>
<body>
<div id="app">
<button v-on:click="handleClick">눌러줘</button>
<p>{{text}}</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
new Vue({
el: '#app',
data: {
text: '',
},
methods: {
handleClick() {
this.text = '눌렀다'
}
},
})
</script>
</body>
</html>
아무곳에나 html 파일을 만들고 위 코드를 넣어서 저장하고 실행해보자. v-on:click
과 같은 특이한 이름의 속성명은 지워져있고, 기능이 제대로 작동됨을 볼수 있다.
Vue는 자기가 템플릿으로 사용할 태그의 HTML 코드를 읽어들인 뒤, Vue 내부에서 관리해야 할 값이나 속성 등을 찾아내서 사용한다.
Vue를 정의하는 자바스크립트 코드에 써둔 '#app' 선택자를 이용해 자신이 사용할 HTML코드가 무엇인지 찾아내며, 해당 HTML코드에서 v-on:
과 같은 Vue 전용 속성 (Vue에선 v-xxx 와 같은 속성명을 디렉티브 라고 부른다)을 찾아서 DOM API로 처리한다.
Vue에도 React처럼 일정 작업환경을 갖추고 작업하는 방식도 존재하며, 이 경우 React와 마찬가지로 브라우저에서 사용되기 전에 미리 DOM 조작용 함수로 변환되는 과정을 거치는게 가능하지만, 템플릿의 문법적 특성은 변함없다.
여러 컴포넌트를 만들고 불러오면서 개발하는 현대적인 SPA 기반 애플리케이션 개발에는 React를 쓰든 Vue를 쓰든 상관없을 것이다.
하지만 서버에서 일일이 HTML이 렌더링되는 레거시 기반 웹사이트에서 특정 페이지 또는 특정 영역만 복잡한 상태관리를 요구하는 프론트엔드 작업이 필요하다고 생각해보자.
React로는 불가능하거나 아주 어려울 것이다. 하지만 Vue는 jQuery 쓰듯이 script 태그로 불러온 다음 특정 html엘레멘트에서 Vue에 의해 화면이 처리되도록 만드는게 가능하다.
React가 지원하는 수많은 기능을 동등하게 지원하는데, 거기에 어느 상황에서도 사용 가능하다니. 그렇다면 Vue는 React의 단점을 깔끔하게 해결하는 React의 완벽한 대체제라 볼수 있을까?
세상일이 다 그렇듯 압도적인 이점은 공짜로 얻어지는게 아니다.
호환성이 전무하다 싶은 React만의 자체문법인 JSX는 개발환경만 제대로 갖추어진다면 활용성이 무궁무진하다. Vue는 호환성을 위해 사실상 자체문법이 가질 이득을 포기한것이라 볼 수 있다.
예를들어 우리가 표 컴포넌트를 구현할 일이 있다고 치자.
표의 각 열마다 종류가 다른 내용이 들어간다. 어떤 열에는 순수한 글씨가 들어갈수도 있지만 어떤 열엔 숫자가 들어갈 수도 있고, 어떤 열엔 예/아니오 가 들어갈 수도 있다.
어떤 곳엔 받은 값으로 링크를 걸어야 할 수도, 어떨땐 입력창을 띄워야 할 수도 있다.
내가 만들 표 컴포넌트를 사용해서 여러종류의 표를 만들어야하고, 그때마다 안에 들어갈 내용이 어떤 종류일지 다르다고 하자. jsx를 사용한다면 첫번째 열부터 마지막 열까지 어떤 식으로 렌더할지 정하도록 jsx를 리턴하는 함수를 참조해주면 된다.
export default Table = ({ rows, columns }) => {
return (
<table>
{rows.map(row => (
<tr>
{columns.map(({ render, key }) => (
<td key={key}>
{render(row[key])}
</td>
))}
</tr>
))}
</table>
)
}
// 표를 사용하는 어떤 페이지
export default Page = () => {
const columns = [
{
key: 'id',
render: (value) => <Link href={`/item/${id}`}>#{id}</Link>
},
{
key: 'name',
render: (value) => (
<input value={value} onChange={(ev) => console.log(ev.target.value)} />
)
},
{
key: 'isGood',
render: (value) => value ? '좋음' : '안좋음'
}
]
const rows = [
{ id: 123, name: '김빵빵', isGood: false },
{ id: 45, name: '강칠칠', isGood: true },
{ id: 678, name: '왕만두', isGood: false },
]
return (
<div>
<Modal columns={columns} rows={rows}/>
</div>
)
}
하지만 Vue는 이런게 아주 어렵다.
Vue를 잘 아는 사람이라면 slot이 이걸 처리해줄거라고 할 것이다. 하지만 slot은 상위 컴포넌트의 템플릿을 하위 컴포넌트에 주입해주는 기능이지 JSX와 같은 태그구조를 값을 다루듯 전달해주는 기능은 아니다.
무슨말이냐 하면, 상위 컴포넌트에서 어떤식으로 렌더할지 전달해야할때, 리액트는 그냥 JSX를 리턴하는 함수를 주입하고 자식 컴포넌트에서 그걸 변수처럼 사용해서 뿌리면 되지만, Vue 의 slot은 자식 컴포넌트에서 정해진 이름/갯수로만 slot을 사용할 수 있어서 동적으로 html을 렌더링 하는게 불가능하다.
결국 이런 상황을 만나게되면 존재가능한 경우의 수 만큼 if문을 만들어 처리해야 한다.
<template>
<table>
<tr v-for="row in rows">
<td v-for="column in columns" key="column">
<slot :column="column" :value="row[column]"></slot>
</td>
</tr>
</table>
</template>
<script>
export default {
data: {
rows: Array,
columns: Array,
},
}
</script>
<template>
<Table :columns="columns" rows="rows">
<template v-slot="{ value, column }">
<Link v-if="column === 'link'" :href="`/item/${id}`">{{id}}</Link>
<input v-if="column === 'name'" :value="value" @change="log($event.target.value)" />
<span v-if="column === 'isGood'">{{value ? '좋음' : '나쁨'}}</span>
</template>
</Table>
</template>
<script>
const columns = ['id','name','isGood']
const rows = [
{ id: 123, name: '김빵빵', isGood: false },
{ id: 45, name: '강칠칠', isGood: true },
{ id: 678, name: '왕만두', isGood: false },
]
export default {
data: {
columns,
rows,
},
methods: {
log(e) {
console.log(e)
}
},
}
</script>
Vue에서도 이런 부분을 인지하고 있는지, 이럴때는 render 함수를 사용하라고 권장하고 있다. 앞서 리액트에 대해 이야기할때 나왔던, DOM 접근을 위해 자동으로 변환된다는 그 함수이다. 어째서인지 순수 DOM보다도 더 사용성이 안좋아보인다.
Vue의 템플릿은 다른 어떤 것들보다도 html과의 호환성에 대해 고려를 더 많이 한 결과물인만큼, 엄연히 자바스크립트와는 거리가 멀어지게 되었다. 때문에, 개발자의 작업생산성에 실제로 영향을 줄 수 있는 유연하고 동적인 처리가 필요한 상황에선 아이러니하게도 효율성이 떨어지게 된다.
Vue에서도 이러한 부분때문에 JSX를 사용할수 있는 방법을 제시해주고 있다. 하지만 태생부터 JSX를 사용하도록 만들어진 React보다 그 효율성이 떨어질 수 밖에 없는 것은 당연하며, 또 당연하게도 그렇게 Vue에서 JSX문법으로 작성한 코드는 일반 HTML에서 호환이 안돼서 React처럼 정해진 개발환경에서만 사용할 수 밖에 없어진다.
React는 호환성을 포기한 대신 개발자에게 높은 자유도를 주었다. 프론트엔드 개발을 진행하는 과정에서 마주칠 수 있는 사실상 모든 상황에 대해 React는 자유롭고 유연한 개발방식을 제공한다.
Vue는 그 자유도에 제한이 있는 대신 높은 호환성을 얻었다. Vue로 작성한 프론트엔드 결과물을 2000년대 초반에 만들어진 레거시 시스템의 페이지의 일부 영역에만 적용하는 것도 가능하다.
두개의 양대 프론트엔드 생태계는, 어느 한쪽이 한쪽을 대체하기엔 지향하는 바가 명확하게 다르다. 우리는 스스로가 필요한 스펙에 따라 둘중 하나를 선택해 사용하면 되는 것이다.
호... 흥미롭네여 잘 읽었음다