๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿ‘จ‍๐Ÿ’ป web.dev/fe

Vanilla JS ๋กœ FigJam ์›น์•ฑ ๋งŒ๋“ค๊ธฐ

by HandHand 2024. 1. 23.

 

 

์ด๋ฒˆ ํฌ์ŠคํŠธ์—์„œ๋Š” ์ž‘๋…„ ์ดˆ์— 4๊ฐœ์›” ์ •๋„ ํ‡ด๊ทผํ•˜๊ณ  ์งฌ ๋‚ด์„œ ๋งŒ๋“  PWA ์›น์•ฑ ๊ฐœ๋ฐœ ๊ณผ์ •์— ๋Œ€ํ•ด ์†Œ๊ฐœํ•ด๋ณผ๊นŒ ํ•ฉ๋‹ˆ๋‹ค.

(๊ฐœ์ธ notion์— ์ •๋ฆฌํ•ด๋†“๊ณ  ์—…๋กœ๋“œ๊ฐ€ ๋„ˆ๋ฌด ๋Šฆ์—ˆ๋„ค์š” ๐Ÿ˜‚)

ํ•ด๋‹น ๋‚ด์šฉ์œผ๋กœ ๊ฒธ์‚ฌ๊ฒธ์‚ฌ ์‚ฌ๋‚ด ์œ„ํด๋ฆฌ ๋ฐœํ‘œ๋„ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

ํ”„๋กœ์ ํŠธ GitHub ์ €์žฅ์†Œ๋Š” ์—ฌ๊ธฐ์„œ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

https://github.com/sohnjunior/editty

 

GitHub - sohnjunior/editty: ๐Ÿง‘‍๐ŸŽจ๐Ÿฆ Sketch with vanilla web app

๐Ÿง‘‍๐ŸŽจ๐Ÿฆ Sketch with vanilla web app. Contribute to sohnjunior/editty development by creating an account on GitHub.

github.com

 

๐Ÿ“Œ ๋“ค์–ด๊ฐ€๋ฉฐ

2022๋…„๋„ ํšŒ๊ณ ๊ธ€์„ ์ž‘์„ฑํ•˜๋ฉด์„œ ์ •ํ–ˆ๋˜ ๋ชฉํ‘œ ์ค‘ ํ•˜๋‚˜๊ฐ€ ์‚ฌ์ด๋“œ ํ”„๋กœ์ ํŠธ ๋ฅผ ์ง„ํ–‰ํ•ด ๋ณด๋Š” ๊ฒƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

๋ฐ”์˜๊ฑฐ๋‚˜ ์‹œ๊ฐ„์ด ๋ถ€์กฑํ•ด์„œ ๋“ฑ๋“ฑ ๋‹ค์–‘ํ•œ ํ•‘๊ณ„๋กœ ๋ฏธ๋ค„์˜จ ๊ณผ์—…์„ ์ด๋ฒˆ์—๋Š” ํ•ด๋ณด๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์ฃผ์ œ๋Š”?

์ด๋•Œ ๋‹น์‹œ ํฅ๋ฏธ๋กœ์šด ์„œ๋น„์Šค๋ฅผ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค.

Easel — a little canvas for any idea

 

Easel — a little canvas for any idea

Store and share any idea you have in a free formed, drag and drop canvas. Support for rich text, images, gifs, embeds, drawings, and more!

goeasel.app

๊ฐ€๋ฒผ์šด ์•„์ด๋””์–ด ์Šค์ผ€์น˜ ์•ฑ์ด๋ผ๊ณ  ๋ณผ ์ˆ˜ ์žˆ๋Š”๋ฐ์š”.

์ €์—๊ฒŒ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ๋งŒ ๋ฝ‘์•„์„œ ๋‹ค์ด์–ด๋ฆฌ & ์•„์ด๋””์–ด ์Šค์ผ€์น˜ ์šฉ๋„์˜ ์•ฑ์„ ๊ฐœ๋ฐœํ•ด ๋ณด๋ฉด ์–ด๋–จ๊นŒ ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค.

์ดˆ์‹ฌ์œผ๋กœ ๋Œ์•„๊ฐ€ ํ‘œ์ค€ 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 ๋“ค์„ ๊ฒฝํ—˜ํ•˜๊ณ  ์‹คํ—˜ํ•ด๋ณด๊ณ  ์‹ถ์œผ์‹  ๋ถ„๋“ค์—๊ฒŒ ์š”๋Ÿฐ ์‚ฌ์ด๋“œ ํ”„๋กœ์ ํŠธ๋ฅผ ์ถ”์ฒœ๋“œ๋ฆฝ๋‹ˆ๋‹ค!

 

๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ™‡‍โ™‚๏ธ

๋ฐ˜์‘ํ˜•

๐Ÿ’ฌ ๋Œ“๊ธ€