React 16 과 Redux 를 이용해서 앱을 개발하다가 손 뗀지 1년이 지나니 다시 시작하기 좀 부담스러웠다. Redux 는 거의 다 잊어버렸고, 액션 리듀서 용어만 어렴풋이 기억났다. State 과 Prop 메카니즘은 잊지 않고 있었다. ES6 문법도 기억났다.
다행히도 어렵지 않게 간단한 PID 시뮬레이터를 React 버전으로 만들 수 있었다.
그러던 중 SVELTE 광고가 자주 눈에 띄인다. 코드량이 적고, Virtual DOM 이 없으며, 진짜 Reactive 하다는 광고로 유혹한다. 이거 정말 괜찮을까? 이걸로 큰 앱을 만들 수 있을까?
부족하나마 React 개발자 관점에서 본 SVELTE 에 대해서 이야기 해보고자 한다.
1. SVELTE 문법
React 의 HTML 는 컴포넌트이며, SVELTE 의 Template 은 컴파일을 통해서 컴포넌트화 된다.
1.1. Script 와 Template 분리
SVELTE 파일은 script 파트와 Template 파트로 나뉘어져있다. Script 에는 자바스크립트가 들어가고 Template 에는 HTML 이 들어가는 구조이다.
Script 와 Temlate 이 분리되어있는 것은 Vue 와 유사하다. React 와 Vue 를 처음 접했을 때, 이 점이 Vue 의 장점이라 생각한 적도 있었다. React 의 JSX 는 처음에는 쉽게 다가오지 않는 면이 있었다.
아무래도 기존의 웹 개발이 Template 에 script 를 입히는 방식이어서 그랬을까? Template 없이 뭔가 한다는 것은 심리적으로 허전하다는 느낌이었다. 무언가를 만들려고 할 때, “어디다 만들지?”, “어디부터 시작할까?” 하는 고민이 들었는데 Template 이 그러한 고민을 다소 해결해 준 것 같다.
SVELTE 는 처음 FE 프레임워크를 접하는 개발자, 백엔드를 하다가 프런트엔드도 하려는 개발자에게 부담스럽지 않게 다가갈 수 있는 도구가 될 수 있을 것이다.
1.2. 하나의 svelte 에 하나의 컴포넌트
SVELTE 의 경우 하나의 컴포넌트가 하나의 svelte 파일에 들어간다. 구조적으로 두개의 컴포넌트를 하나의 svelte 에 넣는 것은 가능하지 않다.
SVELTE 에서 Single file components 관련 이야기가 있는데 어떻게 될 지 궁금하다.
참고 : https://github.com/sveltejs/svelte/issues/2940
반면에 React 의 경우 Template 없으며 컴포넌트는 Class 또는 Function 으로 만들어지므로, 여러개의 컴포넌트를 하나의 js 파일에 넣는 것이 가능하다. 연관성있는 컴포넌트를 하나의 파일에 넣는 것은 코드의 가독성을 향상시킨다.
프로젝트가 커지면 소스파일의 개수가 늘어나는 것도 좋은 모양은 아니다. 이런 면에서는 React 가 좀 더 낫다고 하겠다.
1.3. Template Syntax
Template 안에서의 문법은 {} 안에 들어간다. 변수나 함수 등은 {} 로 감싸서 사용한다. {# 로 시작하는 문법도 있는데 이는 if, each 등의 분기, 순환문을 지원한다.
백엔드에서 쓰던 뷰 Template에서 사용하던 <%=product.name%> 문법과 유사하다.
{#await …} 는 Promise 의 pending, fulfilled, rejected 상태에 따라 다른 rendering 을 보여주는데 이는 SVELTE 의 간결한 Reactivity 를 보여준다고 할 수 있다.
코드를 위한 script 파트가 있어도 Template 에 이러한 문법이 필요한 것은 HTML Template 의 한계라 생각이 든다. React 에서 온 개발자는 싫어할 수 있는 부분이다. (But this is SVELTE-y style)
1.4. JSX 와 Template
React 는 JSX 문법으로 HTML Tag 는 컴포넌트로 구성된다고 이야기했다. React JSX 문법은 HTML Tag 를 아래와 같이 Javascript 코드 안에 넣을 수 있다.
render() {
return (
<p> Hello </p>
)
}
이 표현은 표준 Javascript 에는 없다. 위의 JSX 코드는 아래와 같이 해석된다.
render() {
return
React.createElement('p', {}, 'Hello');
}
JSX 에서는 HTML 요소들이 객체로 존재하므로, 자바스크립트 세계에서 객체가 가지는 모든 혜택을 누릴 수 있다. 그리고 Virtual DOM 은 React Element 의 구조로 되어있다.
SVELTE 의 경우 Template 은 컴파일 과정을 통해 HTML 컴포넌트로 변환된다. 소스코드형태로 있다가 파싱되어서 객체화 되는 것이라고 이해하면 좋을 것이다.
<p> hello </p>
https://svelte.dev/repl/ 에서 JS Output 을 보면 아래와 같이 변환되는 것을 확인할 수 있다.
p = element("p");
p.textContent = "hello";
2. Reactivity
2.1. 반응성
Reactivity 의 사전적 의미는 “반응성” 즉 어떠한 것에 바로 반응할 수 있는 능력이다. Reactivity 를 구성하는 요소에는 “상태”, “뷰"가 있으며 이를 가능하게 해주는 “프레임워크"가 있다. 다시 풀어서 설명하면 어떤 변수(상태) 변화가 있을 때 이를 이용하는(이에 바인딩 되어있는) HTML 컴포넌트(뷰:view)를 (바로) 갱신하는 프레임워크의 능력이라고 이야기할 수 있을 것이다.
웹 프로그래밍 뿐 아니라 다른 프로그래밍에서도 MVP(모델, 뷰 그리고 프리젠터) 이야기를 한다. 모델은 상태이며, 뷰는 HTML컴포넌트, 프리젠터는 Reactivity 를 담당한다고 볼 수 있다.
Reactivity 는 웹에서 갑자기 튀어나온 이야기는 아니다. 모든 GUI 환경에서 Reactivity 는 필요하다. 게임에서 총알이 날라갈 때, 총알이 날아가는 궤적은 모델에 해당하며 실제로 화면상에 그려지는 총알은 뷰에 해당하며 게임 엔진은 궤적을 가지고 화면에 총알을 그리는 프리젠터다.
2.2. 웹에서의 Reactivity 구현 방식
아래와 같은 방법으로 웹 상에서의 Reactivity 를 구현할 수 있다.
A. jQuery 방식 (직접 수정)
- jQuery에는 상태 관리 로직이 들어있지 않고, HTML 컴포넌트를 직접 수정한다.
- 키 이벤트가 발생하면 필요한 HTML 요소를 갱신하는 콜백을 호출한다.
B. React, Vue 방식 (상태 변화 감지)
- 상태 관리 로직이 들어있으며, 이벤트 발생은 상태 변화를 일으키며 프레임워크는 상태 변화에 따른 HTML 요소를 갱신한다.
- 키 이벤트가 발생하면 상태를 업데이트 한다.
- 상태는 Virtual DOM 을 구성하며, 이 Virtual DOM 이 변화되어 HTML 요소를 업데이트 하는 것은 프레임워크의 일이다.
C. SVELTE 방식 (컴파일을 통해 직접 수정)
- 상태 관리 로직이 들어있다. 상태와 연동되는 HTML 요소는 bind 키워드로 표지할 수 있으며 컴파일는 이를 이용해 상태 업데이트 코드를 생성한다.
- 키 이벤트가 발생하면 상태를 업데이트 하며, 상태의 dirty 설정이 바뀌어 HTML 이 갱신된다.
- 컴파일 과정에서 상태의 dirty 가 변하면 HTML 요소를 수정하는 핸들러가 작동하는 코드가 자동적으로 들어간다.
- 결국 HTML 을 직접 수정하도록 되어있다. 따라서 Virtual DOM 을 사용하지 않는다.
jQuery 는 컴포넌트를 직접 수정하는데, 프로그램이 커지면 관리가 어려워지며 다른 관리 방식이 필요해졌다. React 와 SVELTE 에서는 뷰가 바뀌어야 할 상황이 올때 상태를 바꾼다. 상태를 뷰(HTML)로 연결시키는 것은 프레임워크가 알아서 해준다. React 의 경우는 런타임에 Virtual DOM 비교를 통해서 뷰를 업데이트 하며 SVELTE 는 컴파일 시점에 어떤 뷰를 업데이트 할지를 알아서 결정한다.
3. 상태관리
지금까지 SVELTE 의 구조적인 면을 살펴봤는데 이제 중요한 상태관리를 알아보려고 한다.
상태관리의 기본 원칙은 Single Source of Truth (단일 진실 공급원) 이다.
데이터(모델, 상태)의 원천(소스)은 하나여야 한다
한마디 덧붙이자면 그 상태의 변화는 일관되게 관리되어져야 한다.
React 와 같은 FE 프레임워크에서는
- 상위 컴포넌트는 State 를 가지고 있으며 하위 컴포넌트는 State 의 일부를 prop 로 전달받는다.
- 하위 컴포넌트에서 상위 컴포넌트로부터 받은 prop 은 수정할 수 없다. (readonly)
- 상위 컴포넌트는 하위 컴포넌트로 prop 과 state 를 변경할 수 있는 함수를 전달한다.
- State 수정은 상위 컴포넌트에서만 이루어진다. 하위컴포넌트에서는 위의 함수를 통해 상위 컴포넌트의 state 를 변경한다.
이는 Single Source of Truth 를 지키기 위한 구조임에는 틀림 없으나 다소 많은 량의 코드가 필요하다.
반면에 SVELTE 는 “적은 코드량"으로 State 를 만들 수 있다.
3.1. 간결성
SVELTE 에서는 변수 선언만으로 상태가 만들어진다. (즉, 전역변수 = 상태)
<script>
import Child from "./Child.svelte"
let name = "foo";
</script>
<Child {name} />
하위 컴포넌트에서는 아래와 같은 선언으로 상위 컴포넌트의 state 을 사용할 수 있다.
{name} 는 name={name} 과 동일한 표현식이다.
<script>
export let name = "default";
</script>
<p> My name is {name}. </p>
이렇게 하면 하위 컴포넌트에서 name prop 을 쓸 수 있다. 결과는
My name is foo.
당연한 이야기이지만 상위 컴포넌트에서 name 이 변하면 하위 컴포넌트에서 prop 도 같이 바뀐다.
3.2. Prop 이 바뀐다고?
React 의 경우 state 오브젝트를 만들고 이를 갱신하기 위해서 setState 를 써야한다. 상위 컴포넌트으로부터 받은 하위 컴포넌트의 props 는 read only 로 하위 컴포넌트는 이를 직접 수정하지 못한다. (수정하려고 하면 에러가 발생한다.)
이는 하위 컴포넌트에서 상위 컴포넌트로의 의도치 않은 상태변이(오류)를 방지한다.
STELTE 에서는 어떠할까? 변경에 대한 보호장치가 되어있지 않다. 개발자가 실수로 하위컴포넌트에서 prop 을 바꾸는 경우 예기치 못한 오류가 발생할 수 있다. prop 을 literal 로 넘기는 경우는 괜찮지만 Object/Array 로 넘기는 경우 문제가 될 수 있다.
-
Literal prop 예 : https://svelte.dev/repl/a39104c4b28f4c5484f5a7ff6185b619?version=3.29.4
-
Object prop 예 : https://svelte.dev/repl/de584c56e4d84f43a5eac50b36d626e9?version=3.29.4
Click 버튼은 Child 컴포넌트에 있으며, 이 버튼은 Child 컴포넌트의 prop 을 수정한다. 외관상 아무 변화는 없다. 하지만 Object prop 을 넘기는 경우, Reveal 버튼으로 state 값을 찍어보면 값이 바뀐 것을 볼 수 있다.
자바스크립트 뿐만 아니라 다른 언어에서도 Object/Array 은 Reference 타입이며, 이로 인해 다양한 Side Effect 가 발생하는 것은 많이 경험하는 일이다.
Discord 에 이 문제를 이야기해 보았는데,
- “React 에서는 prop 이 readonly 인데 SVELTE 에서는 아닌것 같다. 막을 방법이 없느냐?”
- “너 React 에서 왔구나? 좀 더 SVELTE-y 해져야한다. React 는 잊어라.”
SVELTE 와 React 모두 Reactivity 를 위한 프레임워크이지만, 앞서 이야기한 것처럼 접근 방법은 다르다. 그런 관점에서 보면 React 와 SVELTE 의 다른 점에 큰 의미를 부여할 필요는 없어 보인다.
상태관리를 위해서 Store 를 쓰는 것은 괜찮은 해결책이다. Stores.js 에 State 변화를 추상화시킨 함수를 구현하고 이 함수를 통해서 State 변경을 하도록 강제할 수 있다. 데이터를 캡슐화 시키고, Store 의 함수를 import 하는 부분을 검토하면 디버깅이 좀 용이해 질 것이라 생각이 든다. 하지만 여전히 이 경우에도 Store 에 대한 직접적인 수정은 가능하다.
다만 주의있게 직접 수정하는 일을 하지 않도록 해야한다. Lint 에서 도움을 주면 좋을 것 같다는 생각이 든다.
2.3. Dispatch
React 에서는 State 변경을 위해서 상위 컴포넌트에서 하위 컴포넌트로 state 변경 함수를 prop 통해 전달하는 방식을 쓴다. SVELTE 에서도 같은 방식을 사용할 수 있다.
그리고 SVELTE 는 Event 를 통해서 상위 컴포넌트로 메시지를 보내는 방식을 사용할 수도 있다.
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let color
export let size
</script>
<button on:click={() => dispatch('boxchange', { size: 20, color: 'red' })}>
click
</button>
https://svelte.dev/repl/3af752ae07ae44f58265ecdebc102c1f?version=3.29.7
dispatch 는 바로 상위 컴포넌트에서 이벤트를 받을 수 있으며, 중간 컴포넌트에서는 on:boxchange 를 추가로 적어줘야 한다.
3. 결론
React 와 비교하면서 본의 아니게 SVELTE 의 단점을 부각시킨 것 같지만 실제로는 그렇지 않다. SVELTE 는 글자 그대로 날씬하다. 적은 코드를 가지고 Reactivity 를 만들 수 있으며, 실제로 다운로드 되는 코드량도 작다. 아직 많이 사용해 보지는 않았으나, 빠르게 Reactivity 를 구현하는 데 있어 SVELTE 만한 도구는 이제까지 없었다. 그리고 개인적인 느낌이지만 SVELTE 는 튜토리얼이 이 잘 되어있고, Vue 보다 쉽다.
React 로 만든 PID 시뮬레이터를 SVELTE 로 바꾸어 보았는데 어렵지 않았다. 그 과정 SVELTE 의 이것 저것을 사용해 보았는데 큰 장애는 없었던 것 같다. 약간의 기능을 더 시험해 보고 있는데 앞으로 발전 가능성이 많다고 생각한다.
다만 SVELTE plotly 패키지가 없어서 아직 그래프를 그리지 못하고 있다. React 에 비해 지원하는 패키지가 적다는 점을 염두하고 SVELTE 를 사용해야할 것이다. 이 점은 SVELTE 생태계가 발전할 수록 빠르게 개선되어 갈 것이다.
부족하지만 이 글이 SVELTE 를 이해하고 선택하는데 도움이 되길 바란다.