์ด๋ฒ ํฌ์คํธ์์๋ ์๋ ์ด์ 4๊ฐ์ ์ ๋ ํด๊ทผํ๊ณ ์งฌ ๋ด์ ๋ง๋ PWA ์น์ฑ ๊ฐ๋ฐ ๊ณผ์ ์ ๋ํด ์๊ฐํด๋ณผ๊น ํฉ๋๋ค.
(๊ฐ์ธ notion์ ์ ๋ฆฌํด๋๊ณ ์ ๋ก๋๊ฐ ๋๋ฌด ๋ฆ์๋ค์ ๐)
ํด๋น ๋ด์ฉ์ผ๋ก ๊ฒธ์ฌ๊ฒธ์ฌ ์ฌ๋ด ์ํด๋ฆฌ ๋ฐํ๋ ์งํํ์ต๋๋ค.
ํ๋ก์ ํธ GitHub ์ ์ฅ์๋ ์ฌ๊ธฐ์ ํ์ธํ์ค ์ ์์ต๋๋ค.
https://github.com/sohnjunior/editty
๐ ๋ค์ด๊ฐ๋ฉฐ
2022๋
๋ ํ๊ณ ๊ธ์ ์์ฑํ๋ฉด์ ์ ํ๋ ๋ชฉํ ์ค ํ๋๊ฐ ์ฌ์ด๋ ํ๋ก์ ํธ
๋ฅผ ์งํํด ๋ณด๋ ๊ฒ์ด์์ต๋๋ค.
๋ฐ์๊ฑฐ๋ ์๊ฐ์ด ๋ถ์กฑํด์ ๋ฑ๋ฑ ๋ค์ํ ํ๊ณ๋ก ๋ฏธ๋ค์จ ๊ณผ์ ์ ์ด๋ฒ์๋ ํด๋ณด๊ธฐ๋ก ํ์ต๋๋ค.
๊ทธ๋์ ์ฃผ์ ๋?
์ด๋ ๋น์ ํฅ๋ฏธ๋ก์ด ์๋น์ค๋ฅผ ๋ฐ๊ฒฌํ์ต๋๋ค.
Easel — a little canvas for any idea
๊ฐ๋ฒผ์ด ์์ด๋์ด ์ค์ผ์น ์ฑ์ด๋ผ๊ณ ๋ณผ ์ ์๋๋ฐ์.
์ ์๊ฒ ํ์ํ ๊ธฐ๋ฅ๋ง ๋ฝ์์ ๋ค์ด์ด๋ฆฌ & ์์ด๋์ด ์ค์ผ์น ์ฉ๋์ ์ฑ์ ๊ฐ๋ฐํด ๋ณด๋ฉด ์ด๋จ๊น ์ถ์์ต๋๋ค.
์ด์ฌ์ผ๋ก ๋์๊ฐ ํ์ค Web API ๋ค๊ณผ ๋ฐ๋๋ผTS(?) ์ ํจ๊ป ๋ง์ด์ฃ .
๐ ๊ฐ๋ฐ ๋ก๋๋งต
1๏ธโฃ ์ฑ UX/UI ์ ํ๊ธฐ
UI/UX ๊ฐ ๋ณต์กํ์ง ์์ ์ฑ์ด๋ผ ๋์์ธ ํ ๊ฒ ๋ณ๋ก ์์๋๋ฐ์.
๊ทธ๋๋ ์ฒ์์ ์ฑ ์ ์ฒด์ ์ธ ์ปจ์ ์ ์ก๊ณ ๊ฐ์ผ ํ๋ ๋์์ธ ์๊ฐ์ด ๋ถ์กฑํ ์ ๋ก์๋ ํผ๊ทธ๋ง ์ปค๋ฎค๋ํฐ์ ๋์์ ๋ง์ด ๋ฐ์์ต๋๋ค.
๋ฌด๋ฃ๋ก ์ ๊ณต๋๋ ๋ค์ํ ์ ์ ํ์ผ๋ค๊ณผ ์์ ๋์์ธ ์์๋ค์ด ์์ด์ ์ ์ฉํ๊ฒ ์ฌ์ฉํ ์ ์์์ต๋๋ค.
2๏ธโฃ CI/CD
ํ๋ก์ ํธ ์ค์ ๊ณผ ๋ฐฐํฌ๋ GHA
์ Vercel
์ ํ์ฉํ์ต๋๋ค.
๋ฐฐํฌ๋ฅผ ์ํ ์ค์ ์ด ๊ฐ๋จํ๊ณ GitHub ํ๋ก์ ํธ์ ํธํ์ด ์ข์ ๋ณด์์ต๋๋ค. (๋ํ Vercel
์ ํ๊ตญ ์๋ฒ๋ ์ ๊ณตํด์ฃผ๊ณ ์์ด์์.)
์ ์ฅ์ ์ง์ ํ ๋น๋ ํ๋ผ๋ฏธํฐ๋ง ์ถ๊ฐํด์ฃผ๋ฉด main
๋ธ๋์น๋ก ๋ณํฉ๋ ๋๋ง๋ค ์๋์ผ๋ก ์ด์ํ๊ฒฝ์ ๋ฐฐํฌํด ์ค๋๋ค.
๊ทธ๋ฆฌ๊ณ preview์
production
๋ฒ์ ์ ๋๋ ์ ๋ฐฐํฌํ ์ ์์ด์ ๊ฐ๋ฐ ๋ธ๋์น์์์ ์์
๊ฒฐ๊ณผ๋ฅผ ๋น ๋ฅด๊ฒ ํผ๋๋ฐฑํ ์ ์์ต๋๋ค.
workflow๋ ํฌ๊ฒ 3๊ฐ์ง(client-ci, chromatic, release)๋ก ๋๋ด๋๋ฐ์, client-ci
์ chromatic
์ commit ์ด ๋ฐ์๋๋ ์์ ์ ์ํ๋๋ฉฐ ์ ๋ํ
์คํธ, ์ค๋
์ท ํ
์คํธ๋ฅผ ์ํํ๊ณ lint์ build ์ฑ๊ณต ์ ๋ฌด๋ฅผ ํ์ธํฉ๋๋ค.
๋์ ์ปดํฌ๋ํธ์ ๋ณ๊ฒฝ์ด ์๋ ๊ฒฝ์ฐ ๋ถํ์ํ chromatic ๋ฐฐํฌ๋ฅผ ํผํ๊ธฐ ์ํด src/components
ํ์ ์์ค์ ๋ณ๊ฒฝ์ด ์์ ๊ฒฝ์ฐ์๋ง ์ํํ๋๋ก workflow๋ฅผ ๊ตฌ์ฑํ์ต๋๋ค.
GHA ํ๋ฌ๊ทธ์ธ ์ค์์ changed-files
๋ฅผ ํ์ฉํ๋ฉด ๊ฐ๋จํ๊ฒ ํน์ ๊ฒฝ๋ก ํ์ ํ์ผ ๋ณ๊ฒฝ ์ ๋ฌด๋ฅผ ํ๋จํ ์ ์์ต๋๋ค.
- name: Get changed files
uses: tj-actions/changed-files@v35
id: changed-files
with:
files: client/src/components/**
milestone
workflow์์๋ ๋ฐฐํฌ ๋ฒ์ ๋ฐ ๋ฆด๋ฆฌ์ฆ ๋
ธํธ ์์ฑ์ ๋ด๋นํฉ๋๋ค.
GitHub์์ ์ ๊ณตํ๋ release note ์๋ ์์ฑ ์ต์ ์ ํ์ฉํ๋ฉด PR๊ณผ ์ฐ๊ด๋ ์ด์ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก release note์ compare link๋ฅผ ์์ฑํด์ค๋๋ค.
github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tagName,
name: tagName,
generate_release_notes: true, // โ
์๋์ผ๋ก ๋ฆด๋ฆฌ์ฆ ๋
ธํธ๋ฅผ ์์ฑํฉ๋๋ค.
})
๊ฐ์ธ ํ๋ก์ ํธ์์ ์ฌ์ฉํ๊ธฐ์ ์ด ์ ๋๋ฉด ์ถฉ๋ถํ ๊ฒ ๊ฐ์ต๋๋ค.
๋ฐฐํฌ ๊ด๋ฆฌ๋ milestone
workflow์์ ํ๋ฉฐ ๋ฐฐํฌ ๋ฒ์ ๋ช
์ workflow๋ฅผ trigger ํ ๋ง์ผ์คํค์ ์ ๋ชฉ ์ ๋ณด๋ฅผ ์ฌ์ฉํ์ต๋๋ค.
- name: Get milestone info
id: milestone-info
run: |
echo "NEXT_VERSION=$(jq -r '.milestone.title' $GITHUB_EVENT_PATH)" >> "$GITHUB_OUTPUT"
3๏ธโฃ ํ ์คํธ ํ๊ฒฝ
๊ฐ๋ฐ ์ด๊ธฐ ์ธํ ์ ํ ์คํธ ํ๊ฒฝ๋ ํจ๊ป ๊ตฌ์ถํ์ต๋๋ค.
์๋ํ๋ E2E ํ ์คํธ ๊น์ง๋ ํ์ ์์ด ๋ณด์ด๊ณ , ๋์ ํธ๋ฆฌํ๊ณ ์์ ์ ์ธ ์ปดํฌ๋ํธ ๊ฐ๋ฐ์ ์ํด ์คํ ๋ฆฌ๋ถ ์ค์ ๊ณผ Jest๋ฅผ ์ด์ฉํ DOM ํ ์คํธ๋ฅผ ์งํํ์ต๋๋ค.
์ค๋ ์ท ํ ์คํธ๋ chromatic์ ์ด์ฉํด์ CI ๋จ๊ณ์์ ์ํํ๊ณ ํด๋ผ์ฐ๋์์ ๊ด๋ฆฌ๋๋๋ก ํ์ต๋๋ค.
ํฉ๋ฆฌ์ ์ธ ์กฐ๊ฑด ํ์ ๋ฌด๋ฃ ํ๋์ผ๋ก ์ฌ์ฉํ ์ ์์ด์ ์ข๋ค์ ๐
์ต๊ทผ์ ํฌ๋ก์ค ๋ธ๋ผ์ฐ์ง ํ ์คํธ๋ ๊ฐ๋ฅํ ์ต์ ์ด ์ถ๊ฐ๋์๋๋ฐ, free plan์์๋ ์จ๋ณผ ์ ์์ด์ ์์ฝ์ต๋๋ค.
4๏ธโฃ atomic pattern๊ณผ web component๋ก ์ปดํฌ๋ํธ ๊ฐ๋ฐํ๊ธฐ
์ปดํฌ๋ํธ ์ถ์ํ๋ atomic pattern
์ผ๋ก ์ ๊ทผํ์ต๋๋ค.
๊ฐ์ฅ ๊ณ ๋ฏผ์ด์๋ ๋ถ๋ถ์ molecule
๊ณผ organism
์ ๊ฒฝ๊ณ์๋๋ฐ, ํด๋น ์ปดํฌ๋ํธ๊ฐ ์ธ๋ถ context์ ์์กดํ๊ณ ์๋์ง๋ฅผ ๊ธฐ์ค์ผ๋ก ๋ถ๋ฆฌํ์ต๋๋ค.
์ปดํฌ๋ํธ ๊ตฌํ์ web component
๋ฅผ ์ด์ฉํ์ต๋๋ค. ํ์ค์ ํฌํจ๋ ์ง ์๊ฐ์ด ์ข ์ง๋ ๊ธฐ์ ์ด๋ผ ์ด๋ฐ ๊ฒ ์๋์ง๋ ๋ชฐ๋์ ๋ถ๋ค๋ ๊ณ์
จ์ ๊ฒ ๊ฐ์๋ฐ, ๋ญ๋ ์ง ์ง์ ์จ๋ด์ผ ์๋ฟ๋ ๋ฒ์ด๋ ์ด๋ฒ ๊ธฐํ์ ์ ์ฉํด ๋ดค์ต๋๋ค.
web component
๋ฅผ ํ ์ค๋ก ํํํ๋ฉด ์ถ์ํ๋ ์ปค์คํ
HTML Element๋ฅผ ๋ง๋ค ์ ์๋ ํ์ค ์น ๊ธฐ์
์ ๋๊ฐ ๋ ๊ฒ ๊ฐ์ต๋๋ค. ํฌ๊ฒ 3๊ฐ์ง ํ์ค ์คํ์ผ๋ก ์ฌ์ฌ์ฉ๊ฐ๋ฅํ ์๋ฆฌ๋จผํธ๋ฅผ ์์ฑํ ์ ์๋ ๋ฐฉ๋ฒ์ ์ ๊ณตํฉ๋๋ค.
(custom-element, shadow DOM, HTML template & slot)
shadow DOM
์ผ๋ก ์ธ๋ถ DOM์ ์ํฅ์ ์ฃผ์ง ์๋ ๊ณ ์ ํ ์คํ์ผ์ ๊ฐ์ง๋ ์์ ํ ๋
๋ฆฝ๋ ์๋ฆฌ๋จผํธ๋ฅผ ์์ฑํ ์ ์์ต๋๋ค.
custom element API
๋ก ํ์ค HTMLElement ๊ฐ์ฒด๋ฅผ ํ์ฅํด ์ฌ์ฉ์ ์ ์ ์๋ฆฌ๋จผํธ๋ฅผ ์์ฑํฉ๋๋ค.
๋ง์ฝ ์์ ์๋ฆฌ๋จผํธ์ ๋ํด์ ํ์ฅ์ฑ์ ๋ถ์ฌํ๊ณ ์ถ๋ค๋ฉด slot
์ ์ฌ์ฉํ๋ฉด ๋ฉ๋๋ค.
HTMLElement์์ ํ์ฅํ ์ ์๋ ๋ฉ์๋๋ค์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
class MyElement extends HTMLElement {
// Fires when an instance of the element is created or updated
constructor() {
super();
}
// Fires when an instance was inserted into the document
connectedCallback() {}
// Fires when an instance was removed from the document
disconnectedCallback() {}
// Fires when an attribute was added, removed, or updated
attributeChangedCallback(attrName, oldVal, newVal) {}
// Fires when an element is moved to a new document
adoptedCallback() {}
}
window.customElements.define('my-element', MyElement);
์ด๋ ๊ฒ๋ง ์ฌ์ฉํ๊ธฐ์๋ ๊ธฐ๋ฅ์ด ์ข ๋ถ์กฑํด์ ์ค๋ณต๋ ์ฝ๋๋ฅผ ์ค์ด๊ณ ์ผ๊ด์ฑ ์๋ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค๊ธฐ ์ํด ๋ช ๊ฐ์ง ํธ๋ค๋ฌ์ ๋ผ์ดํ์ฌ์ดํด ๋ฉ์๋๋ค์ ์ถ๊ฐํด ์ค v-component
ํด๋์ค๋ฅผ ๋ง๋ค์์ต๋๋ค.
๋ด๋ถ ๋์์ ๊ฐ๋จํฉ๋๋ค.
ํด๋์ค ์ด๊ธฐํ ์ shadow root
๋ฅผ ํ ๋นํด ์ฃผ๊ณ ์ด๋ฒคํธ ๊ตฌ๋
์ ์ํ ๋ฑ๋ก ํจ์์ create, mount ๋ฑ์ ๋ผ์ดํ์ฌ์ดํด ๋ฉ์๋๋ฅผ ์ถ๊ฐํด ์ค๋๋ค.
ํด๋น ์ปดํฌ๋ํธ๋ก ์์ฑํ ์ปดํฌ๋ํธ์ ์์๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
const template = document.createElement('template')
template.innerHTML = `
<style>
:host #canvas-container {
display: block;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
position: relative;
}
</style>
<div id="canvas-container">
<v-canvas-background-layer></v-canvas-background-layer>
<v-canvas-image-layer></v-canvas-image-layer>
<v-canvas-drawing-layer></v-canvas-drawing-layer>
</div>
`
export default class VCanvasContainer extends VComponent {
static tag = 'v-canvas-container'
private backgroundLayer!: VCanvasBackgroundLayer
private imageLayer!: VCanvasImageLayer
private drawingLayer!: VCanvasDrawingLayer
constructor() {
super(template)
}
get sid() {
return ArchiveContext.state.sid!
}
get snapshots() {
return CanvasDrawingContext.state.snapshots
}
get images() {
return CanvasImageContext.state.images
}
afterCreated() {
this.initLayer()
}
private initLayer() {
const backgroundLayer = this.$shadow.querySelector<VCanvasBackgroundLayer>(
'v-canvas-background-layer'
)
const imageLayer = this.$shadow.querySelector<VCanvasImageLayer>('v-canvas-image-layer')
const drawingLayer = this.$shadow.querySelector<VCanvasDrawingLayer>('v-canvas-drawing-layer')
if (!backgroundLayer || !imageLayer || !drawingLayer) {
console.error('๐จ canvas container need drawing and image layer')
return
}
this.backgroundLayer = backgroundLayer
this.imageLayer = imageLayer
this.drawingLayer = drawingLayer
}
bindEventListener() {
this.drawingLayer.addEventListener('mousedown', this.propagateEventToImageLayer.bind(this))
this.drawingLayer.addEventListener('mousemove', this.propagateEventToImageLayer.bind(this))
this.drawingLayer.addEventListener('mouseup', this.propagateEventToImageLayer.bind(this))
this.drawingLayer.addEventListener('touchstart', this.propagateEventToImageLayer.bind(this))
this.drawingLayer.addEventListener('touchmove', this.propagateEventToImageLayer.bind(this))
this.drawingLayer.addEventListener('touchend', this.propagateEventToImageLayer.bind(this))
}
protected subscribeEventBus() {
EventBus.getInstance().on(EVENT_KEY.SAVE_ARCHIVE, this.onSaveArchive.bind(this))
EventBus.getInstance().on(EVENT_KEY.CLEAR_ALL, this.onClearArchive.bind(this))
EventBus.getInstance().on(EVENT_KEY.CREATE_NEW_ARCHIVE, this.onCreateNewArchive.bind(this))
EventBus.getInstance().on(EVENT_KEY.DOWNLOAD, this.onDownload.bind(this))
}
// ...
}
Vue
์ SFC
์ ๊ต์ฅํ ์ ์ฌํ ํํ๋ก ๋ณด์ด์ง ์๋์?
์ค์ ๋ก Vue
์์๋ template
์ ์ด์ฉํด์ ์ ์ ์ธ ์์ญ์ ๊ตฌ๋ถํ๊ณ ์ด๋ฅผ ํ์ฉํด์ ๋ ๋๋ง ์ฑ๋ฅ ์ต์ ํ๋ฅผ ๋ฌ์ฑํฉ๋๋ค.
web component๋ ์ค์ GitHub, Google๊ณผ ๊ฐ์ ํฐ ๊ท๋ชจ์ ์กฐ์ง์์๋ ์๋น์ค ๊ฐ๋ฐ์ ํ์ฉํ๊ณ ์๋๋ฐ, ์ด๋ค๋ ๊ฐ๊ฐ catalyst์ lit๋ผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๋ง๋ค์ด์ ๋ณต์กํ ์ฑ ๊ฐ๋ฐ์์๋ web component๋ฅผ ์ฉ์ดํ๊ฒ ํ์ฉํ ์ ์๋๋ก ํ์์ผ๋ ๊ด์ฌ์ด ์์ผ์ ๋ถ๋ค์ ํ๋ฒ ์ดํด๋ณด์ ๋ ์ข์ ๊ฒ ๊ฐ์ต๋๋ค.
5๏ธโฃ ์ ์ญ ์ํ ๊ด๋ฆฌ, ์ปดํฌ๋ํธ ํต์ ์ ์ํด context์ event bus ๊ตฌํํ๊ธฐ
์บ๋ฒ์ค ํ์คํ ๋ฆฌ, ์ค์ ๋ฑ์ ์ ์ญ์ํ๋ก ๊ด๋ฆฌํ ํ์๊ฐ ์์๊ณ
ํ์ ์ปดํฌ๋ํธ, ํน์ ๋ถ๋ชจ → ์์์ ์ญ๋ฐฉํฅ ์ด๋ฒคํธ ์ ํ๊ฐ ํ์ํ ์ํฉ์ด ์์์ต๋๋ค.
์ด๋ฌํ ๋ฌธ์ ๋ค์ ํด๊ฒฐํ๊ธฐ ์ํด context
์ event bus
๋ชจ๋์ ๊ตฌํํ์ต๋๋ค.
์ ์ฌํ PUB-SUB
ํจํด์ด์ง๋ง, context๋ ์ํ๊ฐ์ ๊ฐ์ง๋ค๋ ์ฒจ์์ ์ฐจ์ด๋ฅผ ๋ณด์
๋๋ค.
๊ธฐ๋ณธ base context๋ฅผ ํ์ฅํด์ canvas-image
, canvas-drawing
๋ฑ๋ฑ์ context ๋ฅผ ๊ตฌํํฉ๋๋ค.
๋ค์์ ์บ๋ฒ์ค ๊ทธ๋ฆฌ๊ธฐ์ ๊ด๋ จ๋ ํ์คํ ๋ฆฌ๋ฅผ ๋ด๋นํ๋ context ์์์ ๋๋ค.
import { Context } from '@/contexts/shared/context'
import type { Reducer } from '@/contexts/shared/context'
type State = {
snapshots: ImageData[]
stash: ImageData[]
pencilColor: string
strokeSize: number
}
type Action =
| { action: 'PUSH_SNAPSHOT'; data: State['snapshots'] }
| { action: 'HISTORY_INIT'; data: State['snapshots'] }
| { action: 'HISTORY_BACK' }
| { action: 'HISTORY_FORWARD' }
| { action: 'CLEAR_ALL' }
| { action: 'SET_PENCIL_COLOR'; data: State['pencilColor'] }
| { action: 'SET_STROKE_SIZE'; data: State['strokeSize'] }
const initState: State = {
snapshots: [],
stash: [],
pencilColor: 'teal-blue',
strokeSize: 10,
}
const reducer: Reducer<State, Action> = async ({ state, payload }) => {
switch (payload.action) {
case 'PUSH_SNAPSHOT': {
const snapshots = [...state.snapshots]
snapshots.push(...payload.data)
return { ...state, snapshots }
}
case 'HISTORY_INIT': {
const snapshots = payload.data
return { ...state, snapshots }
}
case 'HISTORY_BACK': {
const snapshots = [...state.snapshots]
const stash = [...state.stash]
const snapshot = snapshots.pop()
if (snapshot) {
stash.push(snapshot)
}
return { ...state, snapshots, stash }
}
case 'HISTORY_FORWARD': {
const snapshots = [...state.snapshots]
const stash = [...state.stash]
const snapshot = stash.pop()
if (snapshot) {
snapshots.push(snapshot)
}
return { ...state, snapshots, stash }
}
case 'CLEAR_ALL': {
return { ...state, snapshots: [], stash: [] }
}
case 'SET_PENCIL_COLOR': {
return { ...state, pencilColor: payload.data }
}
case 'SET_STROKE_SIZE': {
return { ...state, strokeSize: payload.data }
}
default:
return { ...state }
}
}
export const CanvasDrawingContext = new Context(initState, reducer)
6๏ธโฃ canvas layer๋ฅผ ๋ถ๋ฆฌํ๊ณ context ๋ก ๊ด๋ฆฌํ๊ธฐ
canvas layer ๋ฅผ ๋ถ๋ฆฌํจ์ผ๋ก์จ ์ป์ ์ ์๋ ์ฅ์ ์ ์ฑ๋ฅ ๊ฐ์ ์ด๋ฉฐ ๋จ์ ์ ํ์คํ ๋ฆฌ ๊ด๋ฆฌ๊ฐ ์ด๋ ค์์ง๋ค๋ ๊ฒ์ผ๋ก ๋ณผ ์ ์์ ๊ฒ ๊ฐ์ต๋๋ค.
๋ค๋ก ๊ฐ๊ธฐ, ์์ผ๋ก ๊ฐ๊ธฐ ๋ฑ์์ ๊ฐ๋ณ canvas context ์ ๋ณด๋ฅผ ๊ฒฐ๊ตญ ํ๋๋ก ํฉ์ณ์ ์๋ณํด ์ค์ผ ์์์ ๋ง๊ฒ ํ์คํ ๋ฆฌ ๊ด๋ฆฌ๊ฐ ๊ฐ๋ฅํ๋ฐ์, ์ด ๋ถ๋ถ๊น์ง๋ ์์ง ๋ฐ์์ด ์ ๋์ด์์ด ์ถํ์ ๊ฐ์ ํด๋ด์ผ ํ ๊ฒ ๊ฐ์ต๋๋ค. ๐
7๏ธโฃ indexedDB๋ก ์บ๋ฒ์ค ๋ฐ์ดํฐ ๊ด๋ฆฌํ๊ธฐ
local ํน์ session ์คํ ๋ฆฌ์ง๋ ๋๊ธฐ์ ์ผ๋ก ์ํ๋ฉ๋๋ค.
์ด๋ ๊ณง ๋ฉ์ธ ์ค๋ ๋๋ฅผ ์ ์ ํ๋ฉฐ, ๋ง์ ์์ ๋ฐ์ดํฐ IO ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ ์ฑ์ ์ธํฐ๋ ์ ์ ๋ฐฉํด๊ฐ ๋ฉ๋๋ค.
์บ๋ฒ์ค ์ด๋ฏธ์ง ๋ฐ ๋๋ก์ ๊ฐ์ฒด์ ๋ฐ์ด๋๋ฆฌ ๋ฐ์ดํฐ๋ ํฌ๊ธฐ๊ฐ ์์ง ์๊ธฐ ๋๋ฌธ์ ๋น๋๊ธฐ์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ read-write ํ ์ ์๋ ๋ฐฉ๋ฒ์ด ํ์ํ์ต๋๋ค.
์ด๋ฌํ ์ํฉ์ ์ํด ์ฌ์ฉํ ์ ์๋ ๊ฒ์ด ๋ฐ๋ก indexedDB
์
๋๋ค.
์ผ๋ฐ ์คํ ๋ฆฌ์ง๋ณด๋ค ์ ์ฅ์ฉ๋๋ ํฌ๊ณ , ์ด๋ฒคํธ ๊ธฐ๋ฐ์ ๋น๋๊ธฐ-๋ ผ๋ธ๋กํน์ผ๋ก ๋์ํ๊ธฐ ๋๋ฌธ์ ์ ์ฅ ๋ฐ ์กฐํ ์ ์ฑ ์ธํฐ๋ ์ ์ ๋ฐฉํดํ์ง ์์ต๋๋ค.
๋ค๋ง callback
๋ฐฉ์์ผ๋ก ๊ตฌํ๋์ด ์๊ธฐ ๋๋ฌธ์ Promise
๋ฐฉ์์ผ๋ก ์ฌ์ฉํ๋ ค๋ฉด ์ ์ ํ ๋ํ์ด ํ์ํฉ๋๋ค.
์ด๋ฌํ ์ฒ๋ฆฌ๋ฅผ ๋์ ํด๋์ ์ ๋ช ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ(https://github.com/jakearchibald/idb)๋ ์์ง๋ง, ๋น๊ต์ ๊ฐ๋จํ ๊ธฐ๋ฅ๋ง ํ์ํ๊ณ , ์ธ๋ถ ์์กด์ฑ ํจํค์ง๋ฅผ ์ต์ํํ๊ธฐ ์ํด ์ง์ ๋ํ ํจ์๋ฅผ ๊ตฌํํ์ต๋๋ค.
indexedDB
์ ๋ํ ์์ธํ ๋ด์ฉ์ MDN์ ์์ธํ ๊ธฐ์ ๋์ด ์์ผ๋ ํ์ํ์ ๋ถ๋ค์ ์ฐธ๊ณ ํ์๋ฉด ๋ ๊ฒ ๊ฐ์ต๋๋ค.
8๏ธโฃ esbuild-loader๋ก webpack ๋น๋ ์๋ ๊ฐ์ ํ๊ธฐ
webpack
๋ฒ๋ค๋ฌ๋ ์์ค ๋ณ๊ฒฝ ์ ์ํธ๋ฆฌ ํฌ์ธํธ๋ถํฐ ๋ค์ ์์กด์ฑ ๊ทธ๋ํ๋ฅผ ์์ฑํ๊ณ ๋ฆฌ๋น๋ฉํฉ๋๋ค.
์ด ๊ณผ์ ์์ ๋ชจ๋ JavaScript ํ์ผ ๋ํ babel-loader(ํน์ TypeScript๋ฅผ ์ฌ์ฉํ๋ค๋ฉด ts-loader ๋) ์ ์ฒ๋ฆฌ ๊ณผ์ ์ ์ํํ๋๋ฐ, ์ด ์ ์ฒ๋ฆฌ ๊ณผ์ ์ ์ต์ ํํด์ ๋น๋ ์๊ฐ์ ๋จ์ถํ ์ ์์ต๋๋ค.
Esbuild
๋ Go
์ธ์ด๋ก ์์ฑ๋ ๋ฒ๋ค๋ฌ์ธ๋ฐ์, ์ด ๋ถ๋ถ์์ JavaScript
์ Go
์ ์ธ์ด์ ์ฐจ์์์ ์ค๋ ์ฐจ์ด์ (์ปดํ์ผ ์์ ๊ณผ ๋ณ๋ ฌ ์ฒ๋ฆฌ)์ ์ํด ์ฑ๋ฅ์ ์ฐจ์ด๊ฐ ์์ต๋๋ค.
loader ํ๋๋ง ๋ฐ๊ฟจ์ ๋ฟ์ธ๋ฐ ์ํฉ์ ๋ฐ๋ผ 2๋ฐฐ~7๋ฐฐ ์ ์๋ ํฅ์์ด ์์์ต๋๋ค.
๋์ esbuild
๋ type cheking์ ํ์ง ์๊ธฐ ๋๋ฌธ์, fork-ts-checker-webpack-plugin
์ ์ฌ์ฉํด์ ๋ณ๋ ํ๋ก์ธ์ค๋ก ํ์
์ ๊ฒ์ฌํ๋ ๋ฐฉ๋ฒ์ด ์์ต๋๋ค.
https://github.com/sohnjunior/editty/pull/81
9๏ธโฃ ์บ๋ฒ์ค ์ค๋ธ์ ํธ ํ์ ๊ตฌํํด ๋ณด๊ธฐ
์บ๋ฒ์ค์์ ์ ๊ณตํ๋ rotate
์ธํฐํ์ด์ค๋ฅผ ์ฌ์ฉํ๋ฉด ์บ๋ฒ์ค ๊ฐ์ฒด ํ์ ์ ํธํ๊ฒ ๊ตฌํ ๊ฐ๋ฅํฉ๋๋ค๋ง..
ํ์ต๋ ํด๋ณผ ๊ฒธ ์ด๋ฒ์๋ ์ด๋ฏธ์ง ์ ์ด์์ญ์ ๋ํ ํ์ ์ฐ์ฐ์ ์ง์ ๊ตฌํํด ๋ดค์ต๋๋ค.
2์ฐจ์ ์ขํํ๋ฉด์์ ํ์ ๋ณํ์ ๋ณํ ํ๋ ฌ
์ ์ฌ์ฉํ๋ฉด ๋ฉ๋๋ค.
2์ฐจ์ ํ๋ฉด์ขํ v(x, y)
์์ ๋ฐ์๊ณ๋ฐฉํฅ์ผ๋ก θ
๋งํผ ํ์ ํ์ ๋, v
๋ฅผ ์ด ๋ฒกํฐ๋ก ํ๋ ๊ณฑ ์ฐ์ฐ์ ํตํด ๋ณํ ์ขํ๊ฐ์ ๊ตฌํ ์ ์์ต๋๋ค.
์ฌ๊ธฐ์ ์์๋ ๊ฒ์ ์บ๋ฒ์ค ์ขํ๊ณ๋ ์ผ๋ฐ์ ์ผ๋ก ์๊ณ ์๋ 2์ฐจ์ ๋ฐ์นด๋ฅดํธ ์ขํ๊ณ์๋ ๋ค๋ฅด๊ฒ ์ข์ธก ์๋จ์ด ์์ ์ธ ์ขํ๊ณต๊ฐ์ผ๋ก ๊ตฌ์ฑ๋๋ค๋ ๊ฒ์
๋๋ค. ๋๋ฌธ์ ํ์ ๊ฐ θ
๋ฅผ ๋ถํธ ๋ณํ ์์ด ์ฌ์ฉํ๋ฉด ์บ๋ฒ์ค ์ขํ๊ณต๊ฐ์์๋ ์๊ณ๋ฐฉํฅ ํ์ ๊ฐ์ผ๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค.
์ ๋ด์ฉ์ ์ฝ๋๋ก ์ฎ๊ธฐ๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
radian
๋ณด๋ค๋ degree
๊ฐ ์ต์ํ ๊ฐ๋ ๋จ์์ด๊ธฐ ๋๋ฌธ์ ๋ณํํด์ ์ฌ์ฉํฉ๋๋ค.
function getRotatedPoint({ point, degree }) {
const { x, y } = point
const radian = degreeToRadian(degree)
const vector = {
x: Math.round(x * Math.cos(radian) - y * Math.sin(radian)),
y: Math.round(x * Math.sin(radian) + y * Math.cos(radian)),
}
return vector
}
function degreeToRadian(degree) {
return (degree * Math.PI) / 180
}
๋ํ์ ํ์ ์ฐ์ฐ์ ๋ค์ 4๊ฐ์ง ๋จ๊ณ๋ก ๋๋ฉ๋๋ค.
1. ๋ํ์ ์ค์ ์ฐพ๊ธฐ
2. ๋ํ์ ์ค์ ์ ์บ๋ฒ์ค ์์ ์ผ๋ก ์ด๋ (๋ํ์ ์ค์ ์ ๊ธฐ์ค์ผ๋ก ํ์ ์ํค๊ธฐ ์ํจ)
3. ๋ํ์ ๊ฐ ๊ผญ์ง์ ์ ๋ณํํ๋ ฌ ์ ์ฉ
4. ๋ํ์ ๋ค์ ์๋์๋ ์์น๋ก ์ฎ๊ธฐ๊ธฐ
์ ๋ฐฉ๋ฒ์ rotate API๋ฅผ ์ด์ฉํ canvas image ๊ฐ์ฒด๋ฅผ ํ์ ์์๋ ๋์ผํ๊ฒ ์ ์ฉ๋ฉ๋๋ค.
๋ง์ง๋ง ํ ๊ฐ์ง ์์ ์ด ๋จ์์ต๋๋ค.
์บ๋ฒ์ค์์ ํฐ์น๋ ์ง์ ์ ๊ธฐ์ค์ผ๋ก ํ์ ๊ฐ (θ
)์ ์ด๋ป๊ฒ ์ ์ ์์๊น์?
์ด๋ฅผ ์ํด์๋ ๋ฒกํฐ์ ๋ฐฉ์๊ฐ์ ๊ตฌํ๋ฉด ๋ฉ๋๋ค.
A
๋ฅผ ๊ธฐ์ค์ผ๋ก ์บ๋ฒ์ค ์ ํ ์ง์ B
์ ๋ํ ๋ฒกํฐ๋ฅผ ์ฌ์ฉํฉ๋๋ค.
์ฌ๊ธฐ์ A
๋ ์ด๋ฏธ์ง์ ์ค์ ์ขํ๊ฐ ๋๊ฒ ๋ค์.
์ ๊ณต์์ ์ฝ๋๋ก ์ฎ๊ธฐ๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
function getBearingDegree(vector: Vector) {
const thetaA = Math.atan2(vector.end.x - vector.begin.x, -vector.end.y + vector.begin.y) // โ
ํจ์์ ์ธ์๋ก ์บ๋ฒ์ค ์ขํ๊ณต๊ฐ์ ์ฌ์ฉํ๊ธฐ ์ํด x์ถ ๋์นญ๋ ์ขํ๊ฐ์ผ๋ก ๋์
const theta = thetaA >= 0 ? thetaA : Math.PI * 2 + thetaA
return Math.floor(radianToDegree(theta))
}
๐ PWA ์ ์ฉํ๊ธฐ
์ด ์ฑ์ ์ค์น ๊ฐ๋ฅํ๊ณ ์คํ๋ผ์ธ์์ ์ ์ฅ ๋ฐ์ดํฐ๊ฐ ๋ฐ์๊ตฌ์ ์ผ๋ก ์ธ ์ ์๋ ๊ฒ์ ๊ณ ๋ คํ์ต๋๋ค.
์ด๋ PWA๋ฅผ ํ์ฉํ ์ ์๋๋ฐ, ๋ค์๊ณผ ๊ฐ์ ํน์ง์ ๊ฐ์ง๋๋ค.
- ๋คํธ์ํฌ์ ์ฌ๋ถ์ ๊ด๊ณ์์ด ์ฑ์ ์ด์ฉํ ์ ์๋ค๋ ์ฅ์
- ์ค์น ๊ฐ๋ฅํ๊ณ ์ผ๋ฐ ๋ธ๋ผ์ฐ์ ํญ ๋์ ์ ๋ ๋ฆฝ์ ์ธ ์คํํ ์ฐฝ์์ ์คํ
- ์น ํธ์ ์๋ฆผ API๋ฅผ ํตํด ๊ฐ์ธํ๋ ์ปจํ ์ธ ๋ก ๊ณ ๊ฐ ์ ์
- ์๋น์ค ์์ปค๋ฅผ ํตํด ๋ฆฌ์์ค ์บ์ ๋ฐ ์คํ๋ผ์ธ ๋์
- URL protocol handling API ๋ฅผ ํตํด์ ์ปค์คํ ์ฑ ์คํด ๋ฑ๋ก ๊ฐ๋ฅ
์๋น์ค ์์ปค๋ ๋ธ๋ผ์ฐ์ ์ ๋คํธ์ํฌ ์ฌ์ด์ ๊ฐ์์ ํ๋ฝ์ ์ญํ ์ ์ํํฉ๋๋ค.
JavaScript ์ฝ๋์ ๋ณ๊ฐ๋ก ๋ณ๋์ ์ค๋ ๋์์ ์คํ๋๊ธฐ ๋๋ฌธ์ non-blocking ๋ฐฉ์์ผ๋ก ์ฌ๋ฌ ์์ ์ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
๋ค๋ง ๋ธ๋ผ์ฐ์ context ์๋ ๋ณ๊ฐ๋ก ๋์ํ๊ธฐ ๋๋ฌธ์ DOM ๊ตฌ์กฐ์ ์ง์ ์ ๊ทผ์ ๋ถ๊ฐ๋ฅํ๊ณ ์ฌ์ฉํ ์ ์๋ API ๊ฐ ์ ํ์ ์ ๋๋ค.
์๋น์ค ์์ปค๋ฅผ ์ด์ฉํ๋ฉด ์ฑ ์คํ์ ํ์ํ ์์ฒญ์ ๊ฐ๋ก์ฑ์ ์บ์ฑ๋ ๋ฆฌ์์ค๋ฅผ ์ ๊ณตํด ์ค์ผ๋ก์จ ์คํ๋ผ์ธ ๊ฒฝํ์ ์ ๊ณตํด ์ค ์ ์์ต๋๋ค.
์ฝํ ์ธ ๋ฅผ ๋ก๋ํ๋ ๋ฐ ํ์ํ ๋คํธ์ํฌ ๋ฆฌ์์ค๋ chrome ๊ถ์ฅ ์ฌํญ์ ๋ฐ๋ผ indexedDB ๋์ cache storage API๋ฅผ ์ฌ์ฉํด์ ์บ์ฑํ์ต๋๋ค.
const ICON_CACHE = [...]
const PAGE_CACHE = [...]
self.addEventListener('install', (ev) => {
ev.waitUntil(cacheResource())
})
async function cacheResource() {
const cache = await caches.open(CACHE_VERSION)
return cache.addAll([...ICON_CACHE, ...PAGE_CACHE])
}
self.addEventListener('fetch', async (ev) => {
if (ev.request.method !== 'GET') {
return
}
ev.respondWith(cacheFirst(ev.request))
})
async function cacheFirst(request) {
const cachedResponse = await caches.match(request)
if (cachedResponse) {
return cachedResponse
}
try {
const response = await fetch(request)
putCache(request, response)
return response
} catch {
return new Response('Network error!', {
status: 408,
headers: { 'Content-Type': 'text/plain' },
})
}
}
async function putCache(request, response) {
const cache = await caches.open(CACHE_VERSION)
cache.put(request, response.clone())
}
์๋น์ค ์์ปค ํ์ผ์ ์์ฑํ๊ณ , install
์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ฉด ๋ฆฌ์์ค ์บ์๋ฅผ ์ํ ์ฝ๋๋ฅผ ์คํํฉ๋๋ค.
fetch
์์ฒญ ์์ ์ด๋ฏธ ์บ์ฑ๋ ์์ฒญ์ด ์๋ค๋ฉด ํด๋น ๋ฆฌ์์ค๋ฅผ ์ฌ์ฉํ๊ณ ๊ทธ๋ ์ง ์๋ค๋ฉด ๋คํธ์ํฌ๋ฅผ ํตํด ๋ฆฌ์์ค๋ฅผ ๋ถ๋ฌ์ ์บ์ฑํฉ๋๋ค.
์ฐธ๊ณ ๋ก ๋ธ๋ผ์ฐ์ ๋ ์๋น์ค ์์ปค ์บ์๋ฅผ HTTP ์บ์๋ณด๋ค ์ฐ์ ํ์ฌ ์ฒ๋ฆฌํฉ๋๋ค.
์ด์ manifest ํ์ผ๋ ์์ฑํฉ๋๋ค. ์ฑ์ ์ค์ ํ์ผ์ด๋ผ๊ณ ๋ณด๋ฉด ๋ฉ๋๋ค.
{
"short_name": "editty",
"name": "editty",
"description": "sketch your idea with vanilla web app",
"icons": [
{
"src": "assets/icons/icon192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "assets/icons/icon512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait"
}
iOS์ ๊ฒฝ์ฐ ์๋๋ก์ด๋์๋ ๋ค๋ฅด๊ฒ manifest.json
๋์ ์ meta
๋ฐ link
ํ๊ทธ๋ค์ ํตํด์ ์น์ฑ ์ค์ ์ ์ถ๊ฐํฉ๋๋ค.
// ์ฑ ์ด๋ฆ
<meta name="apple-mobile-web-app-title" content="editty" />
// ์ฑ ์์ด์ฝ
<link rel="apple-touch-icon" href="assets/icons/icon192.png" />
// ์ฑ ์คํ๋ชจ๋ (full screen)
<meta name="apple-mobile-web-app-capable" content="yes" />
// splash ์ด๋ฏธ์ง
<link rel="apple-touch-startup-image" href="/launch.png">
splash ์ด๋ฏธ์ง๋ ๋๋ฐ์ด์ค ํด์๋์ ์ ํํ ์ผ์นํด์ผ ์ฌ๋ฐ๋ฅด๊ฒ ๋์ํ๋๋ฐ, ์ด๋ link ํ๊ทธ์ media ์์ฑ์ ์ด์ฉํด์ ๋์๋๋ ๋๋ฐ์ด์ค์ ํด๋นํ๋ ๋ฆฌ์์ค๋ง ๋ถ๋ฌ์ค๋๋ก ์์ฑํ๋ฉด ๋ฉ๋๋ค.
๋ค๋ง iOS ๋์์ฉ splash ์ด๋ฏธ์ง๋ฅผ ๋๋ฐ์ด์ค๋ง๋ค ๋ชจ๋ ์ง์ ์์ฑํ๋ ๊ฒ์ด ๋ฒ๊ฑฐ๋กญ๊ธฐ ๋๋ฌธ์, ์ ์๋น์ค๋ฅผ ์ด์ฉํด์ ํ ๋ฒ์ ๋ง๋ค์ด์ ์ฌ์ฉํ๋ ๊ฒ์ ์ถ์ฒํฉ๋๋ค. (์ด๋ฏธ์ง๋ฟ๋ง ์๋๋ผ ๋์๋๋ meta ํ๊ทธ ์ฝ๋๋ค๋ ํจ๊ป ๋ง๋ค์ด์ค๋๋ค.)
์ด์ web app์ด ์ค์น ๊ฐ๋ฅํ๊ณ ์คํ๋ผ์ธ์์๋ ์ฌ์ฉํ ์ ์๊ฒ ๋์์ต๋๋ค!
๐ ๋ง๋ฌด๋ฆฌ
์์ฃผ๋ ์๋๊ฒ ์ง๋ง, ํ์ํ ๊ธฐ๋ฅ ํน์ ๊ฐ์ ๋ ๋งํ ๋ถ๋ถ๋ค์ด ์๊ฐ๋๋ฉด ์๊ฐ์ด ๋ ๋๋ง๋ค ๋ฐ์ํด๋ณด๋ ค๊ณ ํฉ๋๋ค.
ํ๋ก์ ํธ๋ฅผ ํ๋ค ๋ณด๋๊น ์ฌ๋ฏธ์์ด์ ๊ฐ๋ ๋ฐ์ฐจ๋ฅผ ์ฐ๊ณ ๊ฐ๋ฐ์ ํ๊ธฐ๋ ํ์๋๋ฐ,
๊ทธ๋ฌ๋ค ๋ณด๋ ํด๊ฐ๋ฅผ ๋๋ฌด ๋ง์ด ์จ๋ฒ๋ ค์ ์กฐ๊ธ ํํ(?)๋ ํ์ต๋๋ค.. ๐
React์ ๊ฐ์ ์ ์ธํ UI ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์์คํจ์ ๋ค์ ํ๋ฒ ๋๋ผ๊ณ ์ถ์ผ์ ๋ถ๋ค, ์ง์ ์บ๋ฒ์ค๋ฅผ ๋ค๋ค๋ณด๊ณ ์ถ๊ฑฐ๋ ๋ชจ๋ฅด๊ณ ์๋ ๋ค์ํ Web API ๋ค์ ๊ฒฝํํ๊ณ ์คํํด๋ณด๊ณ ์ถ์ผ์ ๋ถ๋ค์๊ฒ ์๋ฐ ์ฌ์ด๋ ํ๋ก์ ํธ๋ฅผ ์ถ์ฒ๋๋ฆฝ๋๋ค!
๊ฐ์ฌํฉ๋๋ค. ๐โ๏ธ
'๐จโ๐ป web.dev > fe' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
rAF ํ๋ ์ ๋น์จ ์กฐ์ ํ๊ธฐ (0) | 2024.01.30 |
---|---|
point-in-polygon ์๊ณ ๋ฆฌ์ฆ์ผ๋ก ๊ตญ๋ด์ธ ํ๋จํ๊ธฐ (0) | 2024.01.30 |
Canvas ๋ํ ํ์ ์ ์๋ฆฌ์ ๊ตฌํ ๋ฐฉ๋ฒ (1) | 2023.08.29 |
Storybook useArgs ํ์ฉํ๊ธฐ (1) | 2023.08.28 |
Web Component์ ํจ๊ป ์ปดํฌ๋ํธ ๋ง๋ค๊ธฐ (0) | 2023.08.25 |
๐ฌ ๋๊ธ