안녕하세요?
오늘은 자바스크립트 프레임워크에 대해 알아 보겠습니다.
20년전 초장기에는 jQuery가 웹 개발에서 차지하는 비중이 대단했었는데요.
React가 나오면서 자바스크립트 프레임워크 세계에서 일대 혁명을 일으켰습니다.
사실 Angular 가 먼저 붐을 일으켰지만 Angular는 너무 어려워서 React가 대세가 되었죠.
React의 Virtual DOM 이 대 유행하면서 Vue가 나왔고, 중국을 위주로 Vue가 React를 추격하는 모양새가 되었지만,
미국을 중심으로한 React의 아성에는 못 미치고 있습니다.
최근에는 Svelte 등 여러가지 자바스크립트 프레임워크가 나오고 있으며, 각각 React의 아성에 도전하려고 노력하고 있습니다.
그래서 오늘은 자바스크립트 Todo 앱을 통해 각각의 프레임워크에 대해 맛보기를 해볼려고 합니다.
어떤 프레임웍을 쓰든 동일한 결과를 빠른 시간안에 도출한다면 그게 제일 좋은 프레임웍이겠죠.
물론, 커뮤니티도 중요합니다. 혼자서는 좋은 웹사이트를 만들 수 없으니까 말이죠.
1. Vanilla Javascript (순수 자바스크립트 버전)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vanilla Todo App</title>
</head>
<body>
<div style="text-align: center">
<h1>Vanilla Todo App</h1>
</div>
</body>
</html>
바닐라 자바스크립트라서 index.html 파일을 만들면 됩니다.
<body>
<div style="text-align: center">
<h1>Vanilla Todo App</h1>
<br />
<ul id="todos"></ul>
<form>
<input name="todo" type="text" />
<input type="submit" value="Add Todo" />
</form>
</div>
</body>
기본적인 form 과 ul 태그를 추가했습니다.
이제 자바스크립트 코드를 작성해 볼까요?
<body>
<div style="text-align: center">
<h1>Vanilla Todo App</h1>
<br />
<ul id="todos"></ul>
<form>
<input name="todo" type="text" />
<input type="submit" value="Add Todo" />
</form>
</div>
<script>
// Get DOM elements
const form = document.querySelector("form");
const input = document.querySelector("[name='todo']");
const todoList = document.getElementById("todos");
// Side Effects / Lifecycle
const existingTodos = JSON.parse(localStorage.getItem("todos")) || [];
const todoData = [];
existingTodos.forEach((todo) => {
addTodo(todo);
});
function addTodo(todoText) {
todoData.push(todoText);
const li = document.createElement("li");
li.innerHTML = todoText;
todoList.appendChild(li);
localStorage.setItem("todos", JSON.stringify(todoData));
input.value = "";
}
// Events
form.onsubmit = (event) => {
event.preventDefault();
addTodo(input.value);
};
</script>
</body>
코드는 간단합니다.
우리가 자바스크립트로 제어할 Dom 엘러먼트를 구합니다.
각각, form, input, totoList 변수에 저장합니다.
그리고 localStorage 에서 기존에 "todos" 항목으로 저장되어 있는걸 불러옵니다.
그리고 우리가 todo 리스트를 저장할 배열을 todoData 선언하고 기존에 localStorage 에 저장되어 있는 걸 forEach 함수로 다시 addTodo함수로 todoData에 추가합니다.
그리고 form.onsubmit 함수에서 event를 이용해서 addTodo 함수를 실행하며 todo 리스트를 todoData에 넣어주는 코드입니다.
바닐라 자바스크립트를 이용한 코드는 요즘은 뭔가 낯설기까지 하는데요.
그래도 이 정도는 알아 두시는게 좋을 듯합니다.
실행화면입니다.
잘 작동되고 있네요.
이제부터는 바닐라 자바스크립트 코드를 잘 생각하면서 다른 자바스크립트 프레임워크가 어떻게 바닐라 자바스크립트처럼 작동하는 Todo 앱을 만드는지 알아봅시다.
2. ReactJS
Create-React-App으로 빈 템플릿을 만들어 볼까요?
npx create-react-app react-todo
Visual Studio Code로 본 React 템플릿입니다.
index.js 를 안 걸들고 바로 메인 컴포넌트인 App.js를 수정해 보겠습니다.
import { useEffect, useRef, useState } from 'react';
import './App.css';
function App() {
// State
const [todos, setTodos] = useState([]);
// Binding
const todoText = useRef();
// Side Effects / Lifecycle
useEffect(() => {
const existingTodos = localStorage.getItem('todos');
setTodos(existingTodos ? JSON.parse(existingTodos) : []);
}, []);
// Events
function addTodo(event) {
event.preventDefault();
const next = [...todos, todoText.current.value];
setTodos(next);
localStorage.setItem('todos', JSON.stringify(next));
}
return (
<div>
<ul>
{todos.map(todo => (<li key={todo}>{todo}</li>))}
</ul>
<form onSubmit={addTodo}>
<input type="text" placeholder="What needs to be done?" ref={todoText} />
<input type="submit" value="Add Todo" />
</form>
</div>
);
}
export default App;
React를 아시는 분은 꽤 쉽게 이해할 수 있습니다.
React에서는 State 관리를 useState 훅을 이용합니다.
State가 변경되면 UI가 다시 그려지게 되는거죠.
그래서 UI가 다시 그려지기 원하시면 useState에 원하는 State를 등록하시면 됩니다.
우리는 todo 라는 State를 등록시켰습니다.
그리고 useEffect 훅을 이용해서 처음 컴포넌트가 로드될때 기존에 localStorage에 있는 todos 항목을 불러오고 그걸 다시 setTodo로 todo State에 추가하고 있습니다.
그리고 코드도 바닐라 자바스크립에서 본 것과 비슷하게 작성되어 있습니다.
실행 화면을 볼까요?
바닐라 자바스크립트와 똑같이 작동하고 있습니다.
3. VueJS
@vue/cli 로 빈 템플릿을 만들어 볼까요?
npx @vue/cli create vue-todo
바로 코드 작성에 들어가기 위해 components 폴더의 HelloWorld.vue 파일을 수정하겠습니다.
VueJS는 기본적으로 .vue 파일에 세개의 태그를 추가할 수 있습니다.
<template>
UI 부분이 여기에 들어갑니다.
</template>
<script>
자바스크립트 코드가 여기에 놓이게 됩니다.
</script>
<style>
CSS 스타일코드가 여기에 들어옵니다.
</style>
그럼 Todo 앱을 작성해 볼까요?
먼저 UI 코드부분입니다.
<template>
<div>
<ul>
<li v-for="todo in todos" v-bind:key="todo">{{ todo }}</li>
</ul>
<form v-on:submit.prevent="addTodo">
<input v-model="todoText" placeholder="What needs to be done?">
<button type="submit">Add Todo</button>
</form>
</div>
</template>
VueJS는 v-for 같은 directive를 사용합니다. Angular 와 비슷하죠.
데이터 바인드도 v-bind 같은 directive를 사용합니다.
React를 하시는 분은 Vue가 처음에는 굉장히 헷갈립니다.
Angular하시다가 Vue하시면 쉽게 적응되지만요.
form 태그에 직접 v-on으로 submit 항목도 넣었고 submit.prevent로 React의 event.preventDefault(); 같은 기능도 바로 구현할 수 있습니다.
이제 자바스크립트 코드를 볼까요?
<script>
export default {
name: "HelloWorld",
data: function () {
return {
todos: [],
todoText: '',
};
},
methods: {
addTodo: function () {
this.todos = [...this.todos, this.todoText];
localStorage.setItem('todos', JSON.stringify(this.todos));
},
},
mounted: function () {
const existingTodos = localStorage.getItem('todos');
this.todos = JSON.parse(existingTodos) || [];
}
};
</script>
VueJS는 export default 로 객체를 내보내는 방식입니다.
그 export한 객체에는 name, data, methods, mounted 같은 변수나 함수를 지정할 수 있구요.
그래서 addTodo 함수를 methods 항목 아래 또다른 객체안에 함수로 정의했습니다.
그리고 컴포넌트가 처음 마운트될때는 mounted: 항목에 넣어구요.
그리고 State는 data 라는 항목에 함수로 지정해야 합니다.
이제 실행해 볼까요?
npm run serve
어떤가요? 잘 작동되죠.
4. AngularJS
이제 Angular에 대해 살펴보겠습니다.
Angular는 굉장히 어려운데요. 특히 TypeScript의 최신 기능을 써야합니다.
빈 템플릿을 만들어 볼까요?
npx @angular/cli new angular-todo
빈 템플릿을 보니까 굉장히 머리가 아파 오는데요.
Google에서 만들었고 실제 Google 웹 앱이 Angular로 만들었다고 하는데, 성능이나 비지니스 스케일에서의 웹 앱 작성에는 탁월하다고 합니다.
일단 우리는 맛보기로 todo앱을 만들겠습니다.
기존에 있는 App 컴포넌트를 수저해서 만들겠는데요.
Angular는 기본적으로 3개의 파일이 한쌍입니다.
.ts 파일은 자바스크립트 코드를 넣는 곳이고(여기서는 타입스크립트겠죠)
.html 파일은 UI 부분을 작성하는 곳입니다.
.css 파일은 CSS 코드를 넣는 곳입니다.
그럼 먼저 .ts 파일을 볼까요?
// app.component.ts 파일
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
// State
todos: string[] = [];
todoText = '';
// Lifecycle
ngOnInit() {
const existingTodos = localStorage.getItem('todos');
this.todos = JSON.parse(existingTodos as string) || [];
}
// Events
addTodo() {
this.todos.push(this.todoText);
localStorage.setItem('todos', JSON.stringify(this.todos));
}
}
Angular 는 @Component 라는 데코레이터를 씁니다.
여기서 selector 같은 div id 를 지정하고, templateUrl 은 UI부분, styleUrls은 CSS 코드입니다.
그리고 Angular은 class 로 작성하는데요.
State는 위 코드처럼 todos: stringp[] = [] 처럼 쓰시면 되고
React의 useEffect 같은 훅은 ngOnInit() 함수에 작성하시면 됩니다.
함수 addTodo() 처럼 원하시는 함수도 그냥 작성하시면 됩니다.
그럼 template UI를 볼까요?
<!-- app.component.html 파일 -->
<ul>
<li *ngFor="let todo of todos">{{todo}}</li>
</ul>
<form (ngSubmit)="addTodo()">
<!-- Binding -->
<input name="todotext" [(ngModel)]="todoText">
<input type="submit" value="Add Todo">
</form>
VueJS 처럼 기본적인 html 태그에 디렉티브를 씁니다.
*ngFor, ngSubmit 같은 디렉티브가 보이고요.
그리도 2-way binding 은 [(ngModel)] 같이 사용합니다.
todoText는 코드부분에 State 로서 선언된 변수이고 그걸 2-way binding 한다는 얘기죠.
그리고, Angular는 app.module.ts 부분에 우리가 쓸 모듈을 지정해 줘야 합니다.
여기서 우리가 쓴 모듈은 바로 ngSubmit 인데요.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule // 추가 된 모듈
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
실행화면을 볼까요?
npm start
잘 실행되고 있습니다.
5. SvelteJS
다섯번째 SvelteJS입니다.
이 프레임웍은 제가 유심히 살펴보고 있는 건데요.
일단 Virtual DOM이 없습니다.
그리고 SvelteJS는 작성된 코드가 그냥 바닐라 코드입니다.
그래서 혹자는 SvelteJS를 TypeScript같은 자바스크립트 컴파일러라고 부르죠.
자바스크립트 본연의 기능을 모두 적용해서 UI부분을 통제하는 거 같습니다.
일단 빈 템플릿을 만들어 보겠습니다.
npx degit sveltejs/template svelte-todo
기본 구조는 다들 비슷합니다.
main.js에서 메인 컴포넌트를 지정하고 있네요.
그리고 App 컴포넌트에서 props를 name 이라고 지정하고 있는데 이렇게 지정된 props는 App 컴포넌트 밑에 있는 모든 컴포넌트에서 name이란 props를 사용할 수 있습니다.
그럼 todo 앱을 만들어 볼까요?
Svelte 는 VueJS처럼 구조가 3가지로 이루어져있습니다.
당연히 .Svelte 확장자를 가진 파일을 사용합니다.
<script>
여기에 자바스크립트 코드가 들어옵니다.
</script>
<style>
여기에 CSS 코드가 들어옵니다.
</style>
그리고 나머지 빈 영역은 바로 일반적인 HTML 코드가 놓이게 됩니다.
VueJS 에서 template 라고 했던걸 SvelteJS에서는 그냥 생략해 버렸습니다.
이제 todo 앱 코드를 볼까요?
<script>
import { onMount } from 'svelte';
let todos = [];
let todoText = '';
onMount(() => {
const existingTodos = localStorage.getItem('todos');
todos = JSON.parse(existingTodos) || [];
});
function addTodo() {
todos = [...todos, todoText];
localStorage.setItem('todos', JSON.stringify(todos));
}
</script>
<main>
<ul>
{#each todos as todo}
<li>{todo}</li>
{/each}
</ul>
<form on:submit|preventDefault={addTodo}>
<input bind:value={todoText} placeholder="What needs to be done?">
<input type="submit" value="Add todo">
</form>
</main>
<script> 태그 속의 자바스크립트 코드 부분은 간단합니다.
바닐라 스크립트랑 똑같습니다.
그리고 React의 useEffect 같은 훅은 svelte 에서 onMount 를 import 하면 됩니다.
그럼 React의 useState 같은 State를 위한 훅은 뭘까요?
SvelteJS는 그냥 let으로 선언한 변수는 모두 State입니다.
간단하죠? Svelte의 막강한 기능입니다.
그리고 UI 부분도 Angular, Vue 처럼 디렉티브를 씁니다.
<form on:submit|preventDefault={addTodo}>
위 코드를 보시면 무엇을 말하는지 이해하시겠죠.
그리고 input 태그의 bind 부분도 bind:value라고 씁니다.
그리고 자바스크립트의 배열 map 함수는 #each 디렉티브를 씁니다.
실행해 볼까요?
npm install
npm run dev
바닐라 자바스크립트와 같습니다.
이번에는 빌드해볼까요?
npm run build
public 폴더에 build 폴더가 생기면서 그 안에 bundle.js 파일이 생겼습니다.
그냥 일반적인 자바스크립트 파일입니다.
bundle.js 파일에 있는 코드입니다.
var app = (function () {
"use strict";
function t() {}
function n(t) {
return t();
}
function e() {
return Object.create(null);
}
function o(t) {
t.forEach(n);
}
function r(t) {
return "function" == typeof t;
}
function u(t, n) {
return t != t
? n == n
: t !== n || (t && "object" == typeof t) || "function" == typeof t;
}
function c(t, n) {
t.appendChild(n);
}
function i(t, n, e) {
t.insertBefore(n, e || null);
}
function l(t) {
t.parentNode.removeChild(t);
}
function f(t) {
return document.createElement(t);
}
function s(t) {
return document.createTextNode(t);
}
function a() {
return s(" ");
}
function d(t, n, e, o) {
return t.addEventListener(n, e, o), () => t.removeEventListener(n, e, o);
}
function p(t, n, e) {
null == e
? t.removeAttribute(n)
: t.getAttribute(n) !== e && t.setAttribute(n, e);
}
function h(t, n) {
t.value = null == n ? "" : n;
}
let m;
function g(t) {
m = t;
}
function $(t) {
(function () {
if (!m)
throw new Error("Function called outside component initialization");
return m;
})().$$.on_mount.push(t);
}
const b = [],
y = [],
_ = [],
v = [],
x = Promise.resolve();
let w = !1;
function E(t) {
_.push(t);
}
let k = !1;
const S = new Set();
function A() {
if (!k) {
k = !0;
do {
for (let t = 0; t < b.length; t += 1) {
const n = b[t];
g(n), N(n.$$);
}
for (g(null), b.length = 0; y.length; ) y.pop()();
for (let t = 0; t < _.length; t += 1) {
const n = _[t];
S.has(n) || (S.add(n), n());
}
_.length = 0;
} while (b.length);
for (; v.length; ) v.pop()();
(w = !1), (k = !1), S.clear();
}
}
function N(t) {
if (null !== t.fragment) {
t.update(), o(t.before_update);
const n = t.dirty;
(t.dirty = [-1]),
t.fragment && t.fragment.p(t.ctx, n),
t.after_update.forEach(E);
}
}
const O = new Set();
function j(t, n) {
-1 === t.$$.dirty[0] &&
(b.push(t), w || ((w = !0), x.then(A)), t.$$.dirty.fill(0)),
(t.$$.dirty[(n / 31) | 0] |= 1 << n % 31);
}
function C(u, c, i, f, s, a, d, p = [-1]) {
const h = m;
g(u);
const $ = (u.$$ = {
fragment: null,
ctx: null,
props: a,
update: t,
not_equal: s,
bound: e(),
on_mount: [],
on_destroy: [],
on_disconnect: [],
before_update: [],
after_update: [],
context: new Map(c.context || (h ? h.$$.context : [])),
callbacks: e(),
dirty: p,
skip_bound: !1,
root: c.target || h.$$.root,
});
d && d($.root);
let b = !1;
if (
(($.ctx = i
? i(u, c.props || {}, (t, n, ...e) => {
const o = e.length ? e[0] : n;
return (
$.ctx &&
s($.ctx[t], ($.ctx[t] = o)) &&
(!$.skip_bound && $.bound[t] && $.bound[t](o), b && j(u, t)),
n
);
})
: []),
$.update(),
(b = !0),
o($.before_update),
($.fragment = !!f && f($.ctx)),
c.target)
) {
if (c.hydrate) {
const t = (function (t) {
return Array.from(t.childNodes);
})(c.target);
$.fragment && $.fragment.l(t), t.forEach(l);
} else $.fragment && $.fragment.c();
c.intro && (y = u.$$.fragment) && y.i && (O.delete(y), y.i(_)),
(function (t, e, u, c) {
const {
fragment: i,
on_mount: l,
on_destroy: f,
after_update: s,
} = t.$$;
i && i.m(e, u),
c ||
E(() => {
const e = l.map(n).filter(r);
f ? f.push(...e) : o(e), (t.$$.on_mount = []);
}),
s.forEach(E);
})(u, c.target, c.anchor, c.customElement),
A();
}
var y, _;
g(h);
}
function I(t, n, e) {
const o = t.slice();
return (o[4] = n[e]), o;
}
function J(t) {
let n,
e,
o = t[4] + "";
return {
c() {
(n = f("li")), (e = s(o));
},
m(t, o) {
i(t, n, o), c(n, e);
},
p(t, n) {
1 & n &&
o !== (o = t[4] + "") &&
(function (t, n) {
(n = "" + n), t.wholeText !== n && (t.data = n);
})(e, o);
},
d(t) {
t && l(n);
},
};
}
function L(n) {
let e,
r,
u,
s,
m,
g,
$,
b,
y,
_ = n[0],
v = [];
for (let t = 0; t < _.length; t += 1) v[t] = J(I(n, _, t));
return {
c() {
(e = f("main")), (r = f("ul"));
for (let t = 0; t < v.length; t += 1) v[t].c();
(u = a()),
(s = f("form")),
(m = f("input")),
(g = a()),
($ = f("input")),
p(m, "placeholder", "What needs to be done?"),
p($, "type", "submit"),
($.value = "Add todo");
},
m(t, o) {
i(t, e, o), c(e, r);
for (let t = 0; t < v.length; t += 1) v[t].m(r, null);
var l;
c(e, u),
c(e, s),
c(s, m),
h(m, n[1]),
c(s, g),
c(s, $),
b ||
((y = [
d(m, "input", n[3]),
d(
s,
"submit",
((l = n[2]),
function (t) {
return t.preventDefault(), l.call(this, t);
})
),
]),
(b = !0));
},
p(t, [n]) {
if (1 & n) {
let e;
for (_ = t[0], e = 0; e < _.length; e += 1) {
const o = I(t, _, e);
v[e] ? v[e].p(o, n) : ((v[e] = J(o)), v[e].c(), v[e].m(r, null));
}
for (; e < v.length; e += 1) v[e].d(1);
v.length = _.length;
}
2 & n && m.value !== t[1] && h(m, t[1]);
},
i: t,
o: t,
d(t) {
t && l(e),
(function (t, n) {
for (let e = 0; e < t.length; e += 1) t[e] && t[e].d(n);
})(v, t),
(b = !1),
o(y);
},
};
}
function T(t, n, e) {
let o = [],
r = "";
return (
$(() => {
const t = localStorage.getItem("todos");
e(0, (o = JSON.parse(t) || []));
}),
[
o,
r,
function () {
e(0, (o = [...o, r])),
localStorage.setItem("todos", JSON.stringify(o));
},
function () {
(r = this.value), e(1, r);
},
]
);
}
return new (class extends class {
$destroy() {
!(function (t, n) {
const e = t.$$;
null !== e.fragment &&
(o(e.on_destroy),
e.fragment && e.fragment.d(n),
(e.on_destroy = e.fragment = null),
(e.ctx = []));
})(this, 1),
(this.$destroy = t);
}
$on(t, n) {
const e = this.$$.callbacks[t] || (this.$$.callbacks[t] = []);
return (
e.push(n),
() => {
const t = e.indexOf(n);
-1 !== t && e.splice(t, 1);
}
);
}
$set(t) {
var n;
this.$$set &&
((n = t), 0 !== Object.keys(n).length) &&
((this.$$.skip_bound = !0), this.$$set(t), (this.$$.skip_bound = !1));
}
} {
constructor(t) {
super(), C(this, t, T, L, u, {});
}
})({ target: document.body, props: { name: "world" } });
})();
//# sourceMappingURL=bundle.js.map
그냥 일반적인 바닐라 자바스크립트입니다.
그래서 SvelteJS를 자바스크립트 컴파일러라고 하는 거 같습니다.
용량도 무지 작습니다.
앞으로 시간나면 틈틈히 SvelteJS를 공부하고 싶어지네요.
지금까지 자바스크립트 프레임워크에 대해 알아봤는데요.
이 외에도 SolidJS, AlpineJS, StelcilJS, mithrillJS, Lit 등 여러가지가 있는데요.
웹 앱을 작성할 때 어떤 프레임웍을 고를지는 순전히 여러분의 뜻에 달렸습니다.
가장 쓰게 편한게 좋은 프레임웍아닐까요?
그래서 ReactJS가 미국에서 가장 커뮤니티가 활성화 되어 있다고 생각합니다.
개인적으로는 SvelteJS를 눈여겨 보고 싶네요.
'코딩 > Javascript' 카테고리의 다른 글
자바스크립트 작동 원리 Execution Context 설명 (0) | 2022.04.05 |
---|---|
자바스크립트 Javascript map, filter, reduce 동작 원리 완전분석 (0) | 2022.04.04 |
자바스크립트 Javascript ES6 NodeJS 모듈 Module exports export default require import 이해 (0) | 2021.09.27 |
자바스크립트 배열 셔플 무작위 섞는 방법 javascript array shuffle (0) | 2021.09.26 |
자바스크립트 함수를 선언하는 6가지 방법 (0) | 2021.08.24 |