이전 글에서 SVELTE에 대해 알아봤다.
SVELTE로 프로젝트 구축하는 방법은 2가지가 있다.
SVELTE REPL은 웹 화면에서 코드를 짜고 바로 테스트해볼 수 있는 도구를 지원해준다. 작성 이후 zip파일로 다운로드 받을 수 있다. 얜 그냥 링크타고 들어가면 바로 테스트해볼 수 있다. >> SVELTE REPL 테스트 웹
리치 해리스, SVELTE 개발자가 만든 패키지로 git에 저장된 svelte/template 패키지를 로컬에 clone하여 새 프로젝트를 생성한다. 녀석 귀엽구만
npm install -g degit
Rollup 🦊 : npx degit sveltejs/template svelte-app
webpack 🐱 : npx degit sveltejs/template-webpack svelte-app
npm install
npm run dev
빰!
🙊 일단 코드 샘플은 REPL 링크에서 진행하겠다.
웹에서 다운로드하던 degit을 통해 clone받던 대충 이런 형태를 띈다. package.json
파일을 보면 SVELTE와 rollup 번들러 디펜던시가 들어있는 것을 확인할 수 있다.
/public/build
: SVELTE가 컴파일하고 Rollup 또는 Webpack이 번들링한 번들파일이 들어있다./src
: 사용자 정의 SVELTE 코드rollup.config.js
: Rollup 모듈 번들러의 설정 파일. Webpack의 경우 webpack.config.js
파일임sirv-cli
: sirv public을 이용해 SPA 서버를 실행
<!-- javascript 코드 -->
<script>
let name = 'durian';
</script><!-- HTML template -->
<h1>Hello {name}!</h1>
<button on:click={() => { name = 'carrots'; }}>근본</button><!-- css style -->
<style>
h1 {
background-color : red;
color : white;
}
</style>
.svelte
파일은 다음과 같이 <script>
, html-template
, <style>
세단계로 나뉘어 구성되어 있다. 딱 봐도 알 수 있듯이 javascript 코드는 <script>
, 렌더링될 HTML은 하단 html-template
, 해당 component에 적용될 CSS는 <style>
에 작성하여 구현하면 된다.
프론트엔드 프레임워크를 다뤄보면서 필수적으로 학습이 필요하다 생각되는 항목만 정리해봤다. 내가 해당 게시글에서 설명하는 내용들은 정말 기초적인 내용이다. 심화된 부분은 튜토리얼가서 직접 테스트해보고 적용하길 바란다. 딹 대라. 기초 문법 들어간다.. 🥕
SVELTE는 반응형 변수 선언부터 초간단하다.
<script>
let name = 'durian';
</script><h1>Hello {name}!</h1><!-- Hello durian! 출력 -->
끝이다.
예? React에서는 setState나 hook을 통해 state 변수를 제어해줘야했다. 근데 SVELTE는 let 변수명;
으로 선언하면 반응형 변수가 생성된다. 와우..
<script>
let name = 'durian';
</script><h1>Hello {name}!</h1>
<button on:click={() => { name = 'carrots'; }}>이름 변경</button> <!-- 버튼-1 -->
<!-- 최초 Hello durian! 출력 -->
<!-- 버튼-1 클릭시 Hello carrots! 업데이트 -->
<!-- state 변수는 react, vueJS와 같이 다양하게 사용 가능 -->
<h1 class={name == 'carrots' ? 'active' : ''}>{name}</h1><img src="" alt={name} /><input type="text" bind:value={name} /><style>
h1 {
color: red;
}
.active {
color: blue;
}
</style>
대표적인 제어문은 크게 조건문과 반복문이 있다. 두 블록의 시작은 # 중간은 : 끝은 /를 붙인다.
#if
로 구현한다. #if 조건문
, :else if 조건문
, :else
로 각 조건을 제어하여 사용한다.<script>
let name = 'carrots';
</script>
<button on:click={() => name = 'durrian'}>헐</button> <!-- 버튼-1 -->
<button on:click={() => name = 'ggingggang'}>헐2</button> <!-- 버튼-2 -->
{#if name == 'carrots'}
<h1>Hello {name} man?!</h1>
{:else if name == 'durrian'}
<h1>Get out {name}!!</h1>
{:else}
<h1>holly shit {name}!!</h1>
{/if}
<!-- 최초 Hello carrots man?! 출력 -->
<!-- 버튼-1 클릭시 Get out durrian!! 업데이트 -->
<!-- 버튼-2 클릭시 holly shit ggingggang!! 업데이트 -->
#each
로 구현한다. 또한 선언된 foods
는 state 변수기 때문에 배열의 변경이 발생하면 바로 적용된다.<script>
let foods = ['🥭', '🥕', '🌽', '🌶'];
const eatFood = () => {
foods = foods.slice(1); // 반응 확인 위해 재할당
}
</script><ul>
{#each foods as food, index}
<li>{index}번 놈 : {food}</li>
{/each}
</ul><button on:click={eatFood}>냠냠냠 이자식아</button> <!-- 버튼-1 -->
<!-- 최초 출력
0번 놈 : 🥭
1번 놈 : 🥕
2번 놈 : 🌽
3번 놈 : 🌶
-->
<!-- 버튼-1 클릭시 업데이트
0번 놈 : 🥕
1번 놈 : 🌽
2번 놈 : 🌶
-->
lifeCycle에 대해 React를 기준으로 설명하면 컴포넌트의 생명 주기에 따라 메소드를 설정해 실행시킬 수 있다. 일단 react의 lifeCycle 메소드를 확인해보자.
import React, { Component } from 'react';
// Basic 컴포넌트를 사용할 때 해당 메소드들이 호출된다. 메소드별 내용을 알아서 찾아보길
export default class Basic extends Component {
/*
생성자 놈
*/
constructor(props) {
super(props);
console.log("constructor");
}
/*
컴포넌트가 DOM 위에 만들어지기 전에 실행
*/
componentWillMount() {
console.log('componentWillMount');
}
/*
컴포넌트 생성 후 렌더링한 뒤 실행
보통 여서 화면 설정 이후 함수 및 AJAX 처리 등을 넣는다.
*/
componentDidMount() {
console.log('componentDidMount');
}
/*
컴포넌트가 prop을 새로 받았을 때 실행
보통 prop에 따라 state를 업데이트할 때 사용하는 메소드로
내부에서 setState() 를 해도 추가적으로 렌더링되지 않는다.
*/
componentWillReceiveProps(nextProps) {
console.log('componentWillReceiveProps');
}
/*
prop 혹은 state 가 변경 되었을 때, 리렌더링 여부를 결정하는 메소드
내부 비즈니스 로직에 따라 true, false 값을 통해 제어한다.
*/
shouldComponentUpdate(nextProps, nextState) {
console.log('shouldComponentUpdate');
return true / false;
}
/*
컴포넌트가 업데이트 되기 전에 실행
🪓 이 메소드 내부에서 setState()를 사용하면 안된다. 무한 루프로 연결되버림
*/
componentWillUpdate(nextProps, nextState) {
console.log('componentWillUpdate');
}
/*
컴포넌트 리렌더링 후 실행
*/
componentDidUpdate(prevProps, prevState) {
console.log('componentDidUpdate');
}
/*
컴포넌트가 DOM 에서 제거될때 실행
*/
componentWillUnmount() {
console.log('componentWillUnmount');
}
....
}
react는 다음과 같은 메소드들을 지원한다. 메소드 이름을 보면 알 수 있듯이 어느정도 유추가 가능하다.
이제 SVELTE의 lifeCycle 메소드를 보자
<script>
import { onMount, beforeUpdate, afterUpdate, onDestroy } from 'svelte'
let name = 'carrots';
/*
변수 값 변경 이전 실행
최초 화면 로드시 onMount보다 먼저 실행
*/
beforeUpdate(() => {
const h1 = document.querySelector('h1');
if (h1 !== null) {
console.log(`before update : ${h1.innerText}`);
} else {
console.log(`before mount`);
}
});
/*
변수 값 변경 이후 실행
최초 화면 로드시 onMount 이후 실행
*/
afterUpdate(() => {
const h1 = document.querySelector('h1');
console.log(`after update : ${h1.innerText}`);
});
/*
컴포넌트 html 렌더링 된 이후 실행
반환함수를 넣으면 onDestory와 같은 기능 (onDestory가 먼저 실행되고 반환함수 실행됨)
onDestory와 return 함수 둘중 하나만 만들어 사용해야함
*/
onMount(() => {
console.log("mounted");
/* 🪓 onMount에서 비동기 함수 로직(async/await)을 넣을 경우
async 함수의 리턴은 promise이므로 return 익명함수가 무시된다.
그래서 비동기 함수가 있는 경우에는 onDestory 훅을 이용해야한다. */
return () => {
console.log('destory');
}
});
/*
컴포넌트가 제거될때 실행
해당 예제에선 onMount return 함수로 onDestroy 구현
*/
// onDestroy(() => console.log('destory');
</script><h1>Hello {name}!</h1>
<button on:click={() => { name = 'durian' }}>안녕</button> <!-- 버튼-1 -->
<!-- 최초 출력 (컴포넌트가 연결되었을 때)
"destory"
"before mount"
"mounted"
"after update : Hello carrots!"
-->
<!-- 버튼-1 클릭시 업데이트
"before update : Hello carrots!"
"after update : Hello durian!"
-->
react보단 단조로운 메소드들을 제공한다. 최초 컴포넌트 생성시 각 메소드는 onDestroy
, beforeUpdate
, onMount
, afterUpdate
순으로 실행되고 변수에 데이터가 업데이트되면 beforeUpdate
와 afterUpdate
가 실행된다.
console.log
에 찍으면 beforeUpdate
와 afterUpdate
둘다 변경된 변수 값을 찍는다. 하지만 변수가 바인딩되어 있는 HTML의 텍스트를 찍어보면 beforeUpdate
에는 변경 전 값이 찍힌다. 👁 이 말인즉 beforeUpdate
와 afterUpdate
는 화면이 렌더링되기 전과 후를 가르키는 것을 알 수 있다.위의 lifeCycle 메소드의 실행과 결과를 보고 알 수 있듯이 변수의 갱신과 화면 DOM에 대한 갱신은 별개다. SVELTE가 화면을 갱신하기 위한 조건은 하나의 테스크가 끝나야 갱신이 이루어진다. 이 말인즉 어떤 특정 버튼을 클릭해서 변수를 바꾼 케이스라면 그 이벤트 함수가 종료되어야 화면 갱신으로 이어진다는 말이다. 이처럼 함수 실행 중 화면 갱신을 보장받을 수 있는 방법이 바로 이 tick을 사용하는 것이다. 예제를 봅시다앙.
<script>
import { tick } from 'svelte'
let name = 'carrots';
const change = async () => {
const h1 = document.querySelector('h1');
name = 'durian';
console.log(h1.innerText);
await tick();
console.log(h1.innerText);
}
</script><h1>Hello {name}!</h1><button on:click={change}>안녕</button> <!-- 버튼-1 -->
<!-- 버튼-1 클릭시 -->
<!-- Hello carrots! 출력 -->
<!-- Hello durian! 출력 -->
위 예제 처럼 tick
기능 사용전엔 변수가 durian
으로 바껴도 h1은 Hello carrots!
을 출력한다. tick
을 사용하여 화면과 변수를 동기화 해준 다음 console.log
를 찍어보면 Hello durian!
이 출력되는 것을 확인할 수 있다.
Component는 각 객체마다 고유의 state를 지닌다. 한마디로 Component 단위로 스코프가 있고 그 영역 내에서 사용할 수 있는 지역 변수가 있다는 말이다. 하지만 전역 변수가 필요한 경우는 어떻게 해야할까. 그거시 이거시다.
stores.js (전역 변수 JS)
import { writable, readable } from 'svelte/store';
/* 쓰기 전용 */
export const count = writable(0, () => {
console.log('count 변수 구독');
return () => {
console.log('count 변수 구독 해제');
}
});
export const name = writable('carrots', () => {
console.log('name 변수 구독');
return () => {
console.log('name 변수 구독 해제');
}
});
/* 읽기 전용 */
export let user = readable({
name: 'carrots',
age: 18
}, (set) => {
console.log('user 변수 구독');
return () => {
console.log('user 변수 구독 해제');
}
});
이런 형태로 모듈을 구성해 스토어 객체를 선언한다. 스토어 객체는 readable
, writable
, derived
형태가 있고 기본적으로subscribe
메소드를 포함한다. 각 스토어 객체는 생성시 초기값과 callback 함수를 설정할 수 있다. callback 함수의 본문은 이 스토어 변수가 참조됐을 때 실행해야하는 코드를 집어넣으면 된다. 테스트해보니 참조할때마다 호출한다기 보단 다른 컴포넌트에서 최초 한번 호출하면 이후 호출은 없다. 그니깐 한군데서라도 쓰는 상황이 발생하면 저 코드가 실행된다. 그리고 최종 return
하는 함수는 구독이 풀렸을 때, 즉 모든 컴포넌트에서 참조하지 않을 때 해당 함수가 실행된다.
🥶 SVELTE STORE 기능은 구독이라는 개념이 존재한다. 이 구독이 뭐냐하면 이 스토어 변수 객체를 참조하여 사용하게되면 구독 상태가 된다. 구독의 개념은 이 스토어 객체의 변결을 감지하는 것을 말한다. 이 구독을 설정할 수 있는 방법은 2가지가 있는데 수동 구독과 자동 구독이 있다.
수동 구독의 경우 변경 감지 subscribe
를 설정하고 컴포넌트 내 지역 변수에 할당하여 사용하는 방식으로 구현한다.
<script>
import { count } from './stores.js';
let countValue;
count.subscribe(value => {
countValue = value;
});
console.log(countValue);
</script>
자동 구독의 경우 import 해온 변수 앞에 $만 붙이면 된다. SVELTE에서도 이 방법을 권장한다. 다만 SVELTE 컴포넌트가 아닌 경우 (.js
, .ts
) 자동 구독을 사용할 수 없기 때문에 수동 구독 기능을 활용하여 사용해야 한다.
<script>
import { count } from './stores.js';
console.log($count);
</script>
App.svelte (메인 화면)
<script>
import { count, name, user } from './stores.js';
import Incrementer from './Incrementer.svelte';
import Decrementer from './Decrementer.svelte';
import Resetter from './Resetter.svelte';
/*
let countValue;
let nameValue;
count.subscribe(value => {
countValue = value;
});
name.subscribe(value => {
nameValue = value;
});
*/
</script><h1>The {$name} count is {$count}</h1><Incrementer/><Decrementer/>
{#if $name == 'carrots'}
<Resetter/>
{/if}
Incrementer.svelte (증가 컴포넌트)
<script>
import { count } from './stores.js';
const increment = () => {
// count.update(n => n + 1);
$count++;
}
</script><button on:click={increment}>+</button>
Decrementer.svelte (감소 컴포넌트)
<script>
import { count } from './stores.js';
const decrement = () => {
// count.update(n => n - 1);
$count--;
}
</script><button on:click={decrement}>-</button>
Resetter.svelte (초기화 컴포넌트)
<script>
import { count, name } from './stores.js';
const reset = () => {
/*
count.set(0);
name.set('durian');
*/
$count = 0;
$name = 'durian';
}
</script><button on:click={reset}>reset</button>
초기화면
각 컴포넌트별로 보면 스토어에 선언된 count
와 name
, user
을 끌어다 활용한다. 수동구독은 subscribe
메소드를 통해 데이터 바인딩 이후 작업한다. 자동 구독의 경우 $
만 앞에 붙이면 일반 변수처럼 사용이 가능하다. 각 컴포넌트에서 변경된 값은 해당 스토어 객체를 사용하는 모든 컴포넌트에 공유된다. 아주 조타~ 👌
역시 프론트엔드 개발에 있어 꽃은 DOM 객체의 이벤트 처리아니겠는가? (난 꽃 알러지가 있다.) SVELTE에서도 DOM 이벤트 제어를 위한 수식어들이 존재한다.
on:이벤트 유형
형태로 정의한다. 모든 이벤트를 다쓰면 오늘 업로드를 못한다. 나머진 알아서 찾아봐라.<script>
let name = 'carrots';
let positions = { x: 0, y: 0 };
</script><!-- click, bind -->
<div id='div_jobdaguri'><h1>{name}</h1><input bind:value="{name}"/><button on:click="{e => name = 'durrian'}">두리안</button><button on:click|once="{e => name = 'ggingggang'}">한번만 낑깡</button></div><!-- mouse evt -->
<!--
on:mouseenter={handleMouseenter}
on:mouseleave={handleMouseleave}
-->
<div id='div_position' on:mousemove="{e => positions = { x: e.clientX, y: e.clientY }}">
마우스 여있다 마~ {positions.x}, {positions.y}
</div><style>
#div_jobdaguri {
width: 100%;
height: 40%;
background-color: blue;
}
#div_position {
width: 100%;
height: 40%;
margin-top:10px;
background-color: red;
}
</style>
코드를 보면 알 수 있듯 우리가 응용하는 이벤트 이름을 넣고 사용해주면 잘된당. 이벤트 속성명 뒤에 특정 이벤트 수식어를 입력해주면 그에 맞게 실행이 제한된다. 신기방긔..
우리가 정의한 이벤트와 곁들여 작용하는 기능으로 기존 addEventListener
에서 제공하는 것인데 거의 동일하다. on:click|once
형태로 사용하며 on:click|once|capture..
처럼 |
로 연결하여 여러개를 사용할 수 있다.
👆 기본 제공 수식어
preventDefault
: 기본 동작 방지stopPropagation
: 이벤트 버블링 방지passive
: 이벤트 처리를 완료하지 않고도 기본 속도로 화면을 스크롤nonpassive
: 명시적인 passive: false
(preventDefault
호출 X)capture
: 캡쳐링에서 이벤트 실행✌ 특수 제어 수식어
once
: 단 한 번만 이벤트 실행, 실행 후 핸들러 삭제self
: target
과 currentTarget
이 일치하는 경우 이벤트 실행react는 state
와 props
를 사용해 컴포넌트끼리 상태를 공유하고 연결한다. SVELTE에서도 당연히 이런 내용이 존재한다. 우리 개발자들은 백번 설명 듣는 것보다 코드로 보는게 빠르다. 일단 코드를 보자. 하하
일반적으로 상위 컴포넌트에서 하위 컴포넌트로 props를 전달하여 구현한다. SVELTE에서는 다음과 같이 사용한다. 아무것도 하지 않으면 기본적으로 단방향으로만 바인딩된다. 한마디로 하위 컴포넌트에서 값을 변경한다고 상위 컴포넌트 값도 변경되지는 않는다는 말이다.
App.svelte
<script>
import Child from './Child.svelte';
</script><Child /><Child sayHi="안녕 건방진?" />
Child.svelte
<script>
export let sayHi = '이게 나야!';
let name = 'carrots';
let age = 18;
</script><h2>{sayHi} 내 이름은 {name}! 탐정이고 {age}살이야!</h2>
결과
다음 구조를 보면 알 수 있듯 부모에서 전달한 정보로 갱신되어 실행된다. 아무 값도 넣어주지 않았을때 비로소 기본 값으로 설정해줬던 이게 나야!
가 출력된다.
아까 위에서 설명한대로 전달받은 하위 컴포넌트에서 오만 똥을 다싸도 상위 컴포넌트의 값이 변경되지는 않는다. 이때 bind
예약어를 통해 작업이 가능하다. 코드를 보자
App.svelte
<script>
import Child from './Child.svelte';
let sayHi;
</script><Child bind:sayHi /><div>App.svelte sayHi : {sayHi}</div>
Child.svelte
<script>
export let sayHi = '이게 나야!';
let name = 'carrots';
let age = 18;
</script><h2>{sayHi} 내 이름은 {name}! 탐정이고 {age}살이야!</h2>
<button on:click={() => { sayHi = '웱'; }}>sayHi 바꿩</button>
결과
bind
를 통해 하위 컴포넌트에서 변경된 값이 상위 컴포넌트에도 적용되었다. 이처럼 양방향 바인딩을 구현할 수 있다.
💥 데이터의 유효범위 관리를 위해 양방향 바인딩을 남발하지 않는 것을 권고함
변수선언 - let 변수명;
제어문 - ,#if 조건문, :else
너무많다.. 졸려서 나중에 또 보자