PWA ํ•˜๋ฃจ ๋งŒ์— ๋„์ž…ํ•˜๊ธฐ(์‚ฝ์งˆ๊ธฐ)

    PWA ํ•˜๋ฃจ ๋งŒ์— ๋„์ž…ํ•˜๊ธฐ(์‚ฝ์งˆ๊ธฐ)


    ์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋Š” ํ•„์ž๊ฐ€ ํšŒ์‚ฌ์—์„œ 2019๋…„ 7์›” 5์ผ ๊ธˆ์š”์ผ ํ•˜๋ฃจ ๋™์•ˆ ๊ธฐ์กด ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์— PWA(Progressive Web Application) ๊ธฐ๋Šฅ์„ ๋ถ™ํžŒ ์‚ฝ์งˆ๊ธฐ๋ฅผ ๊ธฐ๋กํ•˜๋ ค๊ณ  ํ•œ๋‹ค. PWA๋Š” ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋ธŒ๋ผ์šฐ์ €์— ๋Œ€ํ•œ ์˜ˆ์™ธ์ฒ˜๋ฆฌ๋งŒ ๊ผผ๊ผผํ•˜๊ฒŒ ํ•ด์ฃผ๋ฉด UX, ์„ฑ๋Šฅ, SEO ๋“ฑ์—์„œ ๋ฌด์กฐ๊ฑด ํ”Œ๋Ÿฌ์Šค ์š”์ธ์ด๊ธฐ ๋•Œ๋ฌธ์— ์˜ˆ์ „๋ถ€ํ„ฐ ๊ณ„์† ํ•ด๋ณด๊ณ  ์‹ถ์—ˆ๋‹ค.

    ํ•˜์ง€๋งŒ ์‹œ๊ฐ„์ด ์—†์–ด์„œ ๊ณ„์† ๋ฏธ๋ฃจ๊ณ  ์žˆ์—ˆ๋Š”๋ฐ ๋งˆ์นจ ์–ด์ œ ๊ฐ„๋งŒ์— ํ•„์ž์—๊ฒŒ ์—ฌ์œ  ์‹œ๊ฐ„์ด ์ฃผ์–ด์กŒ๋‹ค.

    ํ•„์ž๊ฐ€ ์ง€๊ธˆ ์ž‘์—…ํ•˜๊ณ  ์žˆ๋Š” ํ”„๋กœ์ ํŠธ๊ฐ€ ํšŒ์‚ฌ์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋ชจ๋ธ๊ณผ ๋ฐ€์ ‘ํ•œ ๊ด€๋ จ์ด ์žˆ๋Š” ํ”„๋กœ์ ํŠธ์ด๊ณ , ๋˜ ์›Œ๋‚™ ์ด ๊ธฐ๋Šฅ์— ๊ด€๋ จ๋œ ์‚ฌ๋‚ด ์ดํ•ด๊ด€๊ณ„์ž(Stakeholder)๋“ค์ด ๋งŽ์•„์„œ PO๊ฐ€ ํ…Œ์ŠคํŠธ๋ฅผ ์ข€ ๋” ๊ผผ๊ผผํžˆ ํ•˜๊ณ  ์‹ถ๋‹ค๊ณ  ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

    ๊ทธ๋ž˜์„œ ๊ธˆ์š”์ผ ํ•˜๋ฃจ, ์ •ํ™•ํžˆ ๋งํ•˜๋ฉด ์˜ค์ „์—๋Š” ๋ฒ„๊ทธ ํ„ฐ์ง„๊ฑฐ ํ•˜๋‚˜ ํ•ซํ”ฝ์Šคํ•˜๊ณ  ์ ์‹ฌ๋จน๊ณ ๋‚˜์„œ ์ž๋ฃŒ ์กฐ์‚ฌ๋„ ํ•œ์‹œ๊ฐ„ ์ฏค ํ•ด๋ณธ ๋‹ค์Œ์— 15์‹œ 30๋ถ„ ์ฏค๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ 22์‹œ 55๋ถ„๊นŒ์ง€ ๋‹ฌ๋ ธ๋‹ค. ์›๋ž˜ ํ•„์ž๋Š” ๊ทผ๋ฌด์‹œ๊ฐ„์— ์—ด์‹ฌํžˆ ํ•˜๊ณ  ๋‹ค๋ฅธ ์‹œ๊ฐ„์€ ๋‚˜์—๊ฒŒ ํˆฌ์žํ•˜์ž๋Š” ์ฃผ์˜๋ผ ์•ผ๊ทผ์€ ์™ ๋งŒํ•˜๋ฉด ์•ˆํ•˜๋Š”๋ฐ, ํ•„์ž์—๊ฒŒ ์ฃผ์–ด์ง„ ์‹œ๊ฐ„์ด ํ•˜๋ฃจ ๋ฐ–์— ์—†์–ด์„œ ๊ทธ ์•ˆ์— ๋ฌด์กฐ๊ฑด ๋๋‚ด์•ผ ํ–ˆ๋˜ ๊ฒƒ๋„ ์žˆ์ง€๋งŒ ์‚ฌ์‹ค ์ œ์ผ ํฐ ์ด์œ ๋Š”โ€ฆ

    tyson

    ๋„ค, ์‰ฝ๊ฒŒ ๋ณด๊ณ  ๋ค๋ณ๋‹ค๊ฐ€ ์ณ๋งž์•˜์Šต๋‹ˆ๋‹ค.

    ํ•„์ž๋Š” ์‚ฌ์‹ค PWA๋ฅผ ๊ตฌํ˜„ํ•ด๋ณธ ๊ฒฝํ—˜์ด ์—†๋‹ค. ๊ตฌ๊ธ€์—์„œ ์ œ๊ณตํ•ด์ฃผ๋Š” ๋ฐ๋ชจ๋Š” ๋ช‡๋ฒˆ ๋Œ๋ ค๋ณธ ์ ์ด ์žˆ์ง€๋งŒ ๋‚˜๋จธ์ง€๋Š” ๊ทธ๋ƒฅ ๋‹ค๋ฅธ ๊ต‡์ˆ˜๋ถ„๋“ค์˜ ๋ธ”๋กœ๊ทธ๋ฅผ ๋ณด๊ณ  ์˜ค...๋‚˜๋„ ํ•ด๋ณด๊ณ  ์‹ถ๊ตฐ ์ •๋„๋กœ๋งŒ ์ƒ๊ฐํ–ˆ์—ˆ๋‹ค.

    ๊ทผ๋ฐ ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค์ด ๊ตฌํ˜„ํ•œ ๊ฑธ ๋ณด๋ฉด ๋ญ ์ฝ”๋“œ๊ฐ€ ๋ณต์žกํ•œ ๊ฒƒ๋„ ์•„๋‹ˆ๊ณ  ์„œ๋น„์Šค ์›Œ์ปค(Service Worker)๋„ ์ž‘๋™ ์›๋ฆฌ๊ฐ€ ๊ทธ๋ ‡๊ฒŒ ์ƒ์†Œํ•œ ๋А๋‚Œ์€ ์•„๋‹ˆ๊ธฐ์— ์‚ฌ์‹ค ์–•๋ณด๊ณ  ์žˆ์—ˆ๋‹ค.

    ์›๋ž˜ ํ•„์ž์˜ ๊ณ„ํš

    ์›๋ž˜ ํ•„์ž๋„ PWA์—๊ฒŒ ์ณ๋งž๊ธฐ ์ „๊นŒ์ง„ ๊ทธ๋Ÿด์‹ธํ•œ ๊ณ„ํš์ด ์žˆ์—ˆ๋‹ค. ๋ฌผ๋ก  ํ•„์ž๋Š” ์ž์„ธํžˆ ๊ณต๋ถ€๋ฅผ ํ•˜๊ณ  ๋ค๋น„๋Š” ํƒ€์ž…์ด ์•„๋‹ˆ๋ผ ๊ทธ๋ƒฅ ๋Œ€์ถฉ ์•Œ์•„๋ณธ ๋‹ค์Œ์— ๋‚˜๋จธ์ง€๋Š” ์ง์ ‘ ๋งž์•„๊ฐ€๋ฉด์„œ ๋ฐฐ์šฐ๋Š” ํƒ€์ž…์ด๋ผ ๋” ๊ทธ๋žฌ๋˜ ๊ฒƒ๋„ ์žˆ๋‹ค. ์–ด์จŒ๋“  ์–ด์ œ ์ ์‹ฌ์„ ๋จน๊ณ  ์™€์„œ ํ•„์ž๊ฐ€ ์‚ฌ๋ฌด์‹ค์— ์•‰์•„์„œ ๊ฐ€๋งŒํžˆ ์ƒ๊ฐ์„ ํ•ด๋ณธ ๊ฒฐ๊ณผ, โ€œPWA๋‚˜ ํ•ด๋ณผ๊นŒโ€ฆ? Manifest ๋„ฃ๊ณ  ์„œ๋น„์Šค ์›Œ์ปค ๋ถ™ํžˆ๋ฉด ๋ญ ๋‚˜๋จธ์ง€๋Š” ๋ฌธ์„œ๋ณด๊ณ  ํ•ด๋„ ๋Œ€์ถฉ ๋  ๊ฑฐ ๊ฐ™์€๋ฐโ€ฆ?โ€๋กœ ๊ฒฐ๋ก ์ด ๋‚˜์™”๋‹ค.

    ๊ทธ๋ž˜๋„ PWA์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ๋‹ค ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์€ ํ‡ด๊ทผ ์‹œ๊ฐ„์ธ 19์‹œ๊นŒ์ง€๋Š” ํž˜๋“ค ๊ฒƒ ๊ฐ™์•„์„œ ์ž‘์—…์— ๋Œ€ํ•œ ์Šค์ฝ”ํ”„๋ฅผ ์žก์•˜๋‹ค.

    1. ๊ธฐ๋Šฅ์„ ์ „๋ถ€ ๊ตฌํ˜„ํ•˜๋Š” ๊ฑด ํž˜๋“ค ๊ฒƒ ๊ฐ™์œผ๋‹ˆ ์ผ๋‹จ ๊ธฐ๋ฐ˜์„ ์žก์•„๋†“๋Š”๋‹ค๊ณ  ์ƒ๊ฐํ•˜์ž.
    2. ์„œ๋น„์Šค ์›Œ์ปค ์„ค์น˜.
    3. Manifest.json ์ถ”๊ฐ€. ์ด๊ฑด ๋‚จ๋“ค ๋‹คํ•˜๋Š” ๊ฑฐ๋‹ˆ๊นŒ ์šฐ๋ฆฌ๋„ ๊ธฐ๋ณธ์ ์œผ๋กœ ํ•ด์•ผํ•จ.
    4. Pusher SDK์™€ PushManager๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๊บผ์ ธ์žˆ๋”๋ผ๋„ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ‘ธ์‹œ ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด์—ฌ์ฃผ์ž.
    5. ์‹œ๊ฐ„ ๋˜๋ฉด ๋ชจ๋ฐ”์ผ์—์„œ ํ™ˆ์Šคํฌ๋ฆฐ์— ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ๊นŒ์ง„ ํ•ด๋ณด๊ณ  ์‹ถ๋‹ค.(์šฐ์„ ์ˆœ์œ„ ๋‚ฎ์Œ)
    6. ์˜คํ”„๋ผ์ธ ์บ์‹ฑ์€ ๋‹ค์Œ ์‹œ๊ฐ„์—โ€ฆ(๋‚˜๋ฆ„ ์Šค์ฝ”ํ”„ ์กฐ์ ˆ)

    ๋ฌธ์—๋ฐ˜ ํ”„๋กœ ๊ณ„ํš๋Ÿฌ

    ์ž ์ด์ œ ๊ณ„ํš์ด ์žกํ˜”์œผ๋‹ˆ PO์™€ ํ…Œ์Šคํ„ฐ์—๊ฒŒ โ€œํ”„๋กœ์ ํŠธ ํ…Œ์ŠคํŠธ ํ•˜์‹œ๋Š” ๋™์•ˆ ์ €๋Š” ์ €๋งŒ์˜ ๋†€์ดํ„ฐ์—์„œ ๋†€๋‹ค ์˜ค๊ฒ ์Šต๋‹ˆ๋‹คโ€๋ผ๊ณ  ํ˜‘์˜(๋ผ๊ณ  ์“ฐ๊ณ  ํ†ต๋ณด๋ผ๊ณ  ์ฝ๋Š”๋‹ค.)๋ฅผ ํ•œ ๋’ค ๋งˆ์Šคํ„ฐ์—์„œ ๋ธŒ๋žœ์น˜๋ฅผ ํ•˜๋‚˜ ๋•„๋‹ค. ๋ธŒ๋žœ์น˜ ์ด๋ฆ„๋„ ํ•„์ž์˜ ์˜์ง€๊ฐ€ ๋‹๋ณด์ด๋Š” feature/service-worker-web-push๋กœ ๋”ฑ ์ง€์–ด๋†“๊ณ  15์‹œ 30๋ถ„ ์ฏค ๋ถ€ํ„ฐ ์ž‘์—…์„ ์‹œ์ž‘ํ–ˆ๋‹ค. ๊ทธ๋ž˜๋„ ์ด์ •๋„๋ฉด ํ•œ 4~5์‹œ๊ฐ„ ์•ˆ์— ์ถฉ๋ถ„ํžˆ ๊ฐ€๋Šฅํ•˜๊ฒ ๋‹ค ์‹ถ์—ˆ๋Š”๋ฐโ€ฆ

    babo 코딩하는 내내 필자의 정신상태

    ๊ฒฐ๊ณผ์ ์œผ๋กœ ์ € ์ค‘์—์„œ ๋‹ฌ์„ฑํ•œ ์ œ๋Œ€๋กœ ๋‹ฌ์„ฑํ•œ ๋ชฉํ‘œ๋Š” 1, 2, 3๋ฒˆ ๋ฟ์ด๋‹ค. 4๋ฒˆ ๋ชฉํ‘œ์ธ ํ‘ธ์‹œ ๋ฉ”์‹œ์ง€์˜ ๊ฒฝ์šฐ, Background ๋ฉ”์„ธ์ง•์€ ์‚ฝ์งˆ๋งŒ ํ•˜๋‹ค๊ฐ€ ์‹œ๊ฐ„์ด ๋„ˆ๋ฌด ๋งŽ์ด ๊ฐ€์„œ ์‹คํŒจํ•˜๊ณ  Notification API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Foreground์—์„œ๋งŒ ๋…ธ์ถœ๋˜๋„๋ก ๊ตฌํ˜„ํ–ˆ๋‹ค.

    ์ฒ˜์Œ์—๋Š” ์ˆจ๊ณ  ๋ชจ๋ฐ”์ผ ์•ฑ์—์„œ ์ด๋ฏธ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” FCM(Firebase Cloud Messaging)์„ ์‚ฌ์šฉํ•˜๋ ค๊ณ  ํ–ˆ๋Š”๋ฐ ๊ทธ๋Ÿฌ๋ฉด ์›น ํด๋ผ๋ฆฌ์–ธํŠธ์˜ ํ‘ธ์‹œ ์ฑ„๋„์ด ์ด์›ํ™”๋˜๊ธฐ ๋•Œ๋ฌธ์— ์ผ๋‹จ ํ…Œ์ŠคํŠธ๋„ ํ•ด๋ณผ ๊ฒธ FCM์—†์ด ์„œ๋น„์Šค ์›Œ์ปค์˜ PushManager๋งŒ ์‚ฌ์šฉํ•ด์„œ ๊ตฌํ˜„ํ•˜๋ ค๊ณ  ํ–ˆ๋‹ค.

    ์ด์ œ ํ•„์ž๊ฐ€ ์ด๊ฒƒ๋“ค์„ ์ž‘์—…ํ•˜๋ฉด์„œ ์–ด๋–ค ๋ฌธ์ œ์— ๋ด‰์ฐฉํ–ˆ๊ณ , ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ–ˆ๋Š”์ง€ ํ•œ๋ฒˆ ์„ค๋ช…ํ•ด๋ณด๊ฒ ๋‹ค.

    ์ œ 1 ๊ด€๋ฌธ, Manifest.json

    ์ œ์ผ ์ฒ˜์Œ ํ•œ ์ผ์€ manifest.json์„ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์ด๋‹ค. ์ด๊ฑด ๊ทธ๋ƒฅ ๋ง ๊ทธ๋Œ€๋กœ ์ƒ์„ฑํ•˜๋ฉด ๋œ๋‹ค. ๋˜ํ•œ manifest.json์€ ์–ด์ฐจํ”ผ ์ •์ ์ธ ํŒŒ์ผ์ด๊ธฐ๋„ ํ•˜๊ณ  ์—…๋ฐ์ดํŠธ๋„ ์žฆ์ง€ ์•Š์€ ํŒŒ์ผ์ด๊ธฐ ๋•Œ๋ฌธ์— ๊ตณ์ด Express์—์„œ ์‘๋‹ตํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค. ๊ทธ๋ž˜์„œ ํ”„๋กœ์ ํŠธ ๋‚ด๋ถ€์˜ static ๋””๋ ‰ํ† ๋ฆฌ์— manifest.json์„ ์ƒ์„ฑํ•˜๊ณ  nginx๊ฐ€ ๋ฐ”๋กœ ์‘๋‹ตํ•ด์ฃผ๋Š” ๋ฐฉ์‹์œผ๋กœ ์ž‘์„ฑํ–ˆ๋‹ค.

    // static/manifest.json
    {
      "name": "์ˆจ๊ณ ",
      "short_name": "์ˆจ๊ณ ",
      "icons": [{
        "src": "https://d1hhkexwnh74v.cloudfront.net/app_icons/1x.png",
        "type": "image/png",
        "sizes": "48x48"
      }, {
        "src": "https://d1hhkexwnh74v.cloudfront.net/app_icons/2x.png",
        "type": "image/png",
        "sizes": "64x64"
      }, {
        "src": "https://d1hhkexwnh74v.cloudfront.net/app_icons/3x.png",
        "type": "image/png",
        "sizes": "128x128"
      }, {
        "src": "https://d1hhkexwnh74v.cloudfront.net/app_icons/3x.png",
        "type": "image/png",
        "sizes": "144x144"
      }],
      "start_url": "/?pwa=true",
      "display": "fullscreen",
      "background_color": "#FFFFFF",
      "theme_color": "#00C7AE",
    }

    manifest.json์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ Google Web Developer ๋ฌธ์„œ์˜ The Web App Manifest๋ฅผ ์ฐธ๊ณ ํ•ด์„œ ์ž‘์„ฑํ–ˆ๋‹ค.

    ๊ทธ๋ฆฌ๊ณ  ์ € ์•„์ด์ฝ˜์€ ์ง€๊ธˆ ์ˆจ๊ณ  ์•ฑ์—์„œ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” ์•„์ด์ฝ˜๋“ค์ด๋‹ค. ๊ตณ์ด ๋ชจ๋ฐ”์ผ ์•ฑ๊ณผ ๋‹ค๋ฅธ ์•„์ด์ฝ˜์„ ์‚ฌ์šฉํ•  ์ด์œ ๋„ ์—†๊ณ , ๋””์ž์ด๋„ˆ ๋ถ„๋“ค๋„ ๋ฐ”๋น ์„œ ๋ฉ˜ํƒˆ๋‚˜๊ฐ„ ์ƒํ™ฉ์ด๊ธฐ ๋•Œ๋ฌธ์— ํ•„์ž์˜ ๊ธฐ์ˆ ์ ์ธ ์š•์‹ฌ ๋•Œ๋ฌธ์— ์•„์ด์ฝ˜์„ ๋งŒ๋“ค์–ด ๋‹ฌ๋ผ๊ณ  ํ•˜๊ธฐ์—” ๋„ˆ๋ฌด ๋ฏธ์•ˆํ–ˆ๋‹ค.

    ๋˜ํ•œ ์•ฑ๊ณผ ์›น์ด ๊ฐ™์€ ์•„์ด์ฝ˜์„ ์‚ฌ์šฉํ•ด์•ผ ๋ธŒ๋žœ๋”ฉ ์ธก๋ฉด์—์„œ๋„ ์ข‹์„ ๊ฑฐ๋ผ ์ƒ๊ฐํ•ด์„œ ๋ชจ๋ฐ”์ผ ์•ฑ ๋ ˆํŒŒ์ง€ํ† ๋ฆฌ๋ฅผ ํด๋ก ๋ฐ›์•„์„œ ๋ชฐ๋ž˜ ํ›”์ณ์™”๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ € ์ด๋ฏธ์ง€๋“ค์€ ๊ตณ์ด ํ”„๋กœ์ ํŠธ ๋‚ด์— ์ €์žฅํ•  ํ•„์š”๊ฐ€ ์—†์œผ๋ฏ€๋กœ ํšŒ์‚ฌ์—์„œ ์‚ฌ์šฉํ•˜๋Š” S3 ๋ฒ„ํ‚ท์— ์—…๋กœ๋“œํ•˜๊ณ  CloudFront๋กœ ๋”œ๋ฆฌ๋ฒ„๋ฆฌํ–ˆ๋‹ค.

    ์œ„์—์„œ ์„ค๋ช…ํ–ˆ๋“ฏ์ด ํ•„์ž๋Š” manifest.json์— ๋Œ€ํ•œ ์š”์ฒญ์„ Express๊ฐ€ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์„œ๋ฒ„ ์—”์ง„์ธ nginx๊ฐ€ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์„ ์„ ํƒํ–ˆ๋Š”๋ฐ, ์ด๋ ‡๊ฒŒ nginx๊ฐ€ ์„œ๋ฒ„ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜๊นŒ์ง€ ์š”์ฒญ์„ ํ† ์Šคํ•ด์ฃผ์ง€ ์•Š๊ณ  ๊ทธ๋ƒฅ ์•Œ์•„์„œ ์„œ๋น™ํ•˜๋„๋ก ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ๊ฐ„๋‹จํ•œ ์„ค์ •์„ ์ถ”๊ฐ€ํ•˜๋ฉด ๋œ๋‹ค.

    server {
      ...
      location ~ ^/static {
        root /your/project/location;
      }
      ...
    }

    ์ด๋Ÿฐ ์‹์œผ๋กœ ์„ค์ •ํ•˜๋ฉด ๋ฐ”๋กœ ํ”„๋กœ์ ํŠธ ๋‚ด์˜ static ๋””๋ ‰ํ† ๋ฆฌ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค. โ€/static์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” ์š”์ฒญ์ด ๋“ค์–ด์˜ค๋ฉด /your/project/location ๊ฒฝ๋กœ์—์„œ ๋‹ˆ๊ฐ€ ์•Œ์•„์„œ ์ฐพ์•„์ค˜~โ€œ๋ผ๋Š” ์˜๋ฏธ์ด๋‹ค. ํ•˜์ง€๋งŒ ๋กœ์ปฌ์—์„œ ๊ฐœ๋ฐœ ์„œ๋ฒ„๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ์—๋Š” nginx๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  NodeJS๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ”๋กœ ๊ฐœ๋ฐœ์šฉ ์„œ๋ฒ„๋ฅผ ๋„์šฐ๋ฏ€๋กœ ๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ๋Š” Express๊ฐ€ ์ง์ ‘ ํŒŒ์ผ์„ ์„œ๋น™ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ค˜์•ผ ํ•œ๋‹ค.

    if (process.env.NODE_ENV === 'local') {
        app.use('/static', serve('./static'));
    }

    ์ด์ œ ๋ธŒ๋ผ์šฐ์ €์—๊ฒŒ ๋‚˜์˜ Manifest ํŒŒ์ผ์ด ์—ฌ๊ธฐ ์žˆ์œผ๋‹ˆ ๊ฐ€์ ธ๊ฐ€์‹œ์˜ค๋ผ๊ณ  ์•Œ๋ ค์ค„ ์ˆ˜ ์žˆ๋Š” link ํƒœ๊ทธ๋ฅผ ํ•˜๋‚˜ ๋„ฃ์–ด์ฃผ๋ฉด ๋์ด๋‹ค.

    // constants/meta.constant.js
    export default {
      // ...
      link: [{
          rel: 'manifest',
          href: '/static/manifest.json',
      }],
      // ...
    };
    <link rel="manifest" href="/static/manifest.json" data-vue-meta="true">

    ํ•„์ž๋Š” vue-meta ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ ‡๊ฒŒ Objectํ˜• ๊ฐ์ฒด๋ฅผ ๋ฆฌํ„ดํ•˜๋Š” ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ Œ๋”๋ง ๋•Œ <head> ๋‚ด๋ถ€์— ์•Œ์•„์„œ ๋„ฃ์–ด์ค€๋‹ค. ๊ทธ ํ›„ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ manifest.json์„ ์ œ๋Œ€๋กœ ๊ฐ€์ ธ๊ฐ€๋Š”์ง€๋Š” ํฌ๋กฌ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์˜ Application > Manifest์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

    has manifest

    ์ž, ์—ฌ๊ธฐ๊นŒ์ง€ ํ•˜๋Š”๋ฐ ๊ฑฐ์˜ 10๋ถ„ ์ •๋„ ๊ฑธ๋ ธ๋˜ ๊ฒƒ ๊ฐ™๋‹ค. ์•„์ง๊นŒ์ง€๋Š” ํ•„์ž์˜ ๊ณ„ํš๋Œ€๋กœ ์ˆœํƒ„ํžˆ ํ˜๋Ÿฌ๊ฐ€๊ณ  ์žˆ์—ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ฌธ์ œ๋Š” ์ด์ œ๋ถ€ํ„ฐ ์ƒ๊ธฐ๊ธฐ ์‹œ์ž‘ํ•œ๋‹คโ€ฆ

    ์ œ 2 ๊ด€๋ฌธ, Service Worker

    ๊ธฐ๋ถ„ ์ข‹๊ฒŒ ์ปคํ”ผ ํ•œ์ž” ๋•Œ๋ฆฌ๊ณ  ์™€์„œ ์ด์ œ ์„œ๋น„์Šค ์›Œ์ปค๋ฅผ ์ ์šฉํ•˜๋Š” ์ž‘์—…์„ ์‹œ์ž‘ํ–ˆ๋‹ค. ์„œ๋น„์Šค ์›Œ์ปค๋Š” ๋ธŒ๋ผ์šฐ์ €์— ์„ค์น˜ํ•˜๊ณ  ๋‚˜๋ฉด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์‹คํ–‰๋˜๋Š” ํ”„๋กœ์„ธ์Šค๋กœ, ์›น ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ฉ”์ธ ๋กœ์ง๊ณผ๋Š” ์ „ํ˜€ ๋ณ„๊ฐœ๋กœ ์ž‘๋™ํ•œ๋‹ค.

    ์„œ๋น„์Šค ์›Œ์ปค์™€ ์›น์•ฑ์˜ ๋ฉ”์ธ ๋กœ์ง์€ ์„œ๋กœ ๋ฉ”์„ธ์ง€๋ฅผ ์ฃผ๊ณ  ๋ฐ›๋Š” ๋ฐฉ์‹์œผ๋กœ ์ž‘๋™ํ•œ๋‹ค. postMessage๋กœ ๋ณด๋‚ด๋ฉด onMessage๋กœ ๋ฐ›๋Š” ๋ฐฉ์‹์ธ๋ฐ, WebView๋‚˜ Chrome Extension๊ณผ ๊ฐ™์€ ๋ฐฉ์‹์ด๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฏธ ์šฐ๋ฆฌ์—๊ฒ ์ต์ˆ™ํ•œ ๋ฐฉ์‹์ด๋‹ค.

    ๋‹จ, ์•„์ง๊นŒ์ง€ ๋ชจ๋“  ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ง€์›๋˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ฐ˜๋“œ์‹œ navigator ์ „์—ญ ๊ฐ์ฒด ๋‚ด๋ถ€์— serviceWorker ๊ฐ์ฒด๊ฐ€ ์กด์žฌํ•˜๋Š” ์ง€ ํ™•์ธํ•œ ํ›„ ์ดˆ๊ธฐํ™”๋ฅผ ์ง„ํ–‰ํ•ด์•ผํ•œ๋‹ค.

    if ('serviceWorker' in navigator) { /* ... */ }

    ๋ญ ์ด ์ •๋„ ์ฏค์ด์•ผ ํ•„์ž๊ฐ™์€ ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๋“ค์—๊ฒŒ๋Š” ๊ต‰์žฅํžˆ ์ต์ˆ™ํ•œ ์ƒํ™ฉ์ด๊ธฐ ๋•Œ๋ฌธ์— ๊ทธ๋ƒฅ ์›ƒ๊ณ  ๋„˜๊ธธ ์ˆ˜ ์žˆ์ง€๋งŒ ์ด๊ฒƒ๋ณด๋‹ค ๋” ๊ท€์ฐฎ์€ ๊ฒŒ ์žˆ์—ˆ๋‹ค. ๋ฐ”๋กœ ์„œ๋น„์Šค ์›Œ์ปค์˜ ๊ฐœ๋ฐœํ™˜๊ฒฝ ์„ธํŒ…์ด๋‹ค.

    ๊ฐœ๋ฐœํ™˜๊ฒฝ ์„ธํŒ…

    ์„œ๋น„์Šค ์›Œ์ปค๋Š” ์ผ๋ฐ˜์ ์ธ ์›น ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜๋ณด๋‹ค ๋งŽ์€ ๊ธฐ๋Šฅ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ณด์•ˆ์ด ๊ต‰์žฅํžˆ ์ค‘์š”ํ•˜๋‹ค. ๊ทธ๋ž˜์„œ ์„œ๋น„์Šค ์›Œ์ปค๋Š” ์ œํ•œ์ ์ธ ํ™˜๊ฒฝ์—์„œ๋งŒ ์ž‘๋™ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋งŒ๋“ค์–ด์กŒ๋Š”๋ฐ ๊ทธ ์กฐ๊ฑด์€ ๋”ฑ ๋‘๊ฐ€์ง€์ด๋‹ค.


    1. ์›น ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ํ˜ธ์ŠคํŠธ๊ฐ€ localhost์ด๊ฑฐ๋‚˜
    2. HTTPS ํ”„๋กœํ† ์ฝœ์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์„ ๊ฒƒ

    ๋ถˆํ–‰ํžˆ๋„ ํ•„์ž๋Š” ์ด ๋‘๊ฐ€์ง€ ๋ชจ๋‘ ๋‹ค ํ•ด๋‹น์ด ์•ˆ๋œ๋‹ค. ํ•„์ž์˜ ํšŒ์‚ฌ๋Š” ๋กœ์ปฌ์—์„œ http://local.soomgo.com์ด๋ผ๋Š” ์˜ค๋ฆฌ์ง„์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์„œ๋น„์Šค ์›Œ์ปค ๊ฐ์ฒด๊ฐ€ ํ™œ์„ฑํ™” ๋˜์ง€ ์•Š๋Š”๋‹ค. ๊ทธ๋Ÿผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์€ 2๊ฐ€์ง€๋กœ ์ค„์–ด๋“ ๋‹ค. ๋‚ด๊ฐ€ localhost๋กœ ์ ‘์†ํ•˜๊ฑฐ๋‚˜ ๋กœ์ปฌ์— HTTPS๋ฅผ ๋ถ™ํžˆ๊ฑฐ๋‚˜.

    ์‚ฌ์‹ค ํ•„์ž๋Š” ์ด๊ฑธ ๋†“์ณ์„œ โ€œ์•„ ์„œ๋น„์Šค ์›Œ์ปค ์–ด๋””๊ฐ”์–ด? ์™œ ์•ˆ๋ผ? ์ฃฝ์„๋ž˜?โ€๋กœ ์ปดํ“จํ„ฐ๋ž‘ ์‹ค๋ž‘์ดํ•˜๋А๋ผ ํ•œ 30๋ถ„ ๋‚ ๋ ค๋จน์—ˆ๋‹ค. ๊ณต์‹ ๋ฌธ์„œ์— ๋ฒ„์ “์ด ํ•œ๊ตญ์–ด๋กœ ์ ํ˜€์žˆ๋Š” ๋‚ด์šฉ์ด๋ฏ€๋กœ ํ•ญ์ƒ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ๊ผผ๊ผผํžˆ ์ฝ์žโ€ฆ

    ๊ฐœ๋ฐœ ์„œ๋ฒ„๋ฅผ HTTPS๋กœ ๋„์šฐ์ž!

    ๊ทผ๋ฐ ๋˜ ํ•„์ž๊ฐ€ localhost๋กœ ๋งž์ถ”๊ธฐ์—๋Š” ์™ ์ง€ ์ปดํ“จํ„ฐ ๋”ฐ์œ„์—๊ฒŒ ์ง€๋Š” ๊ธฐ๋ถ„์ด๋ผ ๊ทธ๋ƒฅ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์— HTTPS๋ฅผ ๋ถ™ํžˆ๊ธฐ๋กœ ํ–ˆ๋‹ค.

    Express๋Š” ๋‚ด์žฅ ๊ฐ์ฒด๋กœ https ๊ฐ์ฒด๋ฅผ ์ง€์›ํ•ด์ฃผ๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ„๋‹จํ•˜๊ฒŒ ์…‹์—…ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋จผ์ € HTTPS๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” SSH ํ‚ค์Œ์ด ํ•„์š”ํ•˜๋ฏ€๋กœ openssl์„ ์‚ฌ์šฉํ•˜์—ฌ ํ‚ค๋ฅผ ๋งŒ๋“ค์–ด ์ฃผ์—ˆ๋‹ค.

    $ openssl genrsa 1024 > private.pem # ๋น„๊ณต๊ฐœํ‚ค ์ƒ์„ฑ
    ...
    $ openssl req -x509 -new -key private.pem > public.pem # ๊ณต๊ฐœํ‚ค ์ƒ์„ฑ

    ๊ทธ ๋‹ค์Œ ์ด ํ‚ค๋ฅผ ํ”„๋กœ์ ํŠธ์˜ ์ ๋‹นํ•œ ๊ณณ์— ์ €์žฅํ•œ ๋‹ค์Œ ๊ทธ๋ƒฅ ์‚ฌ์šฉํ•˜๋ฉด ๋˜๋Š”๋ฐ ์—ฌ๊ธฐ์„œ ์ฃผ์˜ ์‚ฌํ•ญ.

    > ์ƒ์„ฑํ•œ sshํ‚ค๋Š” ๋กœ์ปฌ ๊ฐœ๋ฐœํ™˜๊ฒฝ์—์„œ๋งŒ ์‚ฌ์šฉ๋˜์–ด์•ผํ•ฉ๋‹ˆ๋‹ค.
    > ์ด ๋””๋ ‰ํ† ๋ฆฌ ๋‚ด์˜ *.pem ํŒŒ์ผ์€ gitignore์— ์ถ”๊ฐ€๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค.

    /keys/README.md

    ์ ˆ๋Œ€ ๋ฆฌ๋ชจํŠธ ์ €์žฅ์†Œ์— SSH ํ‚ค๋ฅผ ์—…๋กœ๋“œํ•˜์ง€ ๋ง์ž. ๋ณด์•ˆ ํ„ฐ์ง„๋‹ค. ๋ฌผ๋ก  ์ด ํ‚ค๋กœ ๋ญ”๊ฐ€ ์‹ค์งˆ์ ์ธ ์ธ์ฆ์„ ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ๋กœ์ปฌ ๊ฐœ๋ฐœ ์„œ๋ฒ„์—์„œ๋งŒ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ญ”๊ฐ€ ์ค‘์š”ํ•œ ๊ฑธ ํ„ธ๋ฆด ์—ผ๋ ค๋Š” ์—†์ง€๋งŒ ๊ทธ๋ž˜๋„ ์Šต๊ด€์ด ์ค‘์š”ํ•˜๋‹ค. SSH ํ‚ค๋Š” ํญํƒ„ ๋‹ค๋ฃจ๋“ฏ์ด ๋‹ค๋ฃจ์ž.

    ๋Œ€์‹  ํ•„์ž๋Š” ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋“ค์ด ์‰ฝ๊ฒŒ HTTPS ํ™˜๊ฒฝ์„ ์„ธํŒ…ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ”„๋กœ์ ํŠธ ๋‚ด์— ํ‚ค๋ฅผ ์ €์žฅํ•˜๋Š” ๋””๋ ‰ํ† ๋ฆฌ ๋‚ด๋ถ€์— README.md๋ฅผ ์ž‘์„ฑํ•ด๋†“๊ณ  ์ด ํ‚ค๊ฐ€ ์–ด๋–ค ์šฉ๋„๋กœ ์‚ฌ์šฉ๋  ๊ฒƒ์ธ์ง€ ์ฃผ์˜ ์‚ฌํ•ญ์€ ๋ฌด์—‡์ธ์ง€ ์ƒ์„ธํ•˜๊ฒŒ ์ ์–ด๋†“์•˜๋‹ค.

    ๊ทธ๋ฆฌ๊ณ  ๊ฐœ๋ฐœํ•  ๋•Œ๋Š” HTTPS๊ฐ€ ์•„๋‹ˆ๋ผ HTTP๋กœ ์„œ๋ฒ„๋ฅผ ๋„์šธ ์ผ๋„ ์ƒ๊ธธ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๊ฐœ๋ฐœ ์„œ๋ฒ„๋ฅผ ๋„์šธ ๋•Œ ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ํ”„๋กœํ† ์ฝœ์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ฃผ์—ˆ๋‹ค.

    // package.json
    {
      "scripts": {
        "serve": "cross-env NODE_ENV=local node server",
        "serve:https": "cross-env NODE_ENV=local node server --https",
      }
    }
    // server.js
    const express = require('express');
    const https = require('https');
    
    // node์˜ ์˜ต์…˜๋“ค์€ ๋ฐฐ์—ด ํ˜•ํƒœ๋กœ process.argv์— ๋‹ด๊ฒจ์žˆ๋‹ค.
    const useHttps = process.argv.some(val => val === '--https');
    
    const app = express();
    
    // ...
    if (isLocal) {
      let localApp = app;
      if (useHttps) {
        localApp = https.createServer({
          // ์•„๊นŒ ์ƒ์„ฑํ•œ ํ‚ค๋Š” ์—ฌ๊ธฐ์„œ ์‚ฌ์šฉํ•œ๋‹ค!
          key: fs.readFileSync('./keys/private.pem'),
          cert: fs.readFileSync('./keys/public.pem'),
        }, app);
      }
      localApp.listen(port, host, () => {
        debug(`${isHttps ? 'https://' : 'http://'}${host}:${port}๋กœ ๊ฐ€์ฆˆ์•„!!!!`);
      });
    }
    // ...

    ์ด๋Ÿฐ ์‹์œผ๋กœ ์ž‘์„ฑํ•ด๋†“์œผ๋ฉด ๊ฐœ๋ฐœ ์„œ๋ฒ„๋ฅผ ์˜ฌ๋ฆด ๋•Œ ์›ํ•˜๋Š” ํ”„๋กœํ† ์ฝœ์„ ์ž์œ ์ž์žฌ๋กœ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ํ˜น์‹œ ๋‚ด๊ฐ€ ๊ฐœ๋ฐœ ์„œ๋ฒ„๋ฅผ HTTPS๋กœ ๋ฐ”๊ฟ”๋†”์„œ ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž ์ปดํ“จํ„ฐ์—์„œ ๊ฐœ๋ฐœ ์„œ๋ฒ„๊ฐ€ ์•ˆ ์˜ฌ๋ผ๊ฐ€๋„ ์ผ๋‹จ ์‹œ๊ฐ„์„ ๋ฒŒ์–ด๋†“๊ณ  ๋ฒ„๊ทธ๋ฅผ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

    ์›๋ž˜ ์ง„์งœ ๊ณ ์ˆ˜๋Š” ๋งž๊ธฐ ์ „์— ๋งž์„ ๊ณณ์„ ์˜ˆ์ƒํ•ด์„œ ๋ฏธ๋ฆฌ ํž˜์„ ์ฃผ๊ณ  ์žˆ๋Š” ๋ฒ•์ด๋‹ค.(๋งŽ์ด ๋งž์•„๋ณธ 1์ธ)

    ๊ฐœ๋ฐœ ์„œ๋ฒ„๋ฅผ HTTPS๋กœ ๋„์šฐ๋ฉด ์ด์ œ navigator ๊ฐ์ฒด ๋‚ด๋ถ€์— ์ด์˜๊ฒŒ ๋“ค์–ด๊ฐ€์žˆ๋Š” serviceWorker ๊ฐ์ฒด๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

    > navigator.serviceWorker
    < ServiceWorkerContainer {ready: Promise, controller: null, oncontrollerchange: null, onmessage: null}

    HTTPS๋ฅผ ์“ฐ์ง€ ์•Š๊ณ  ๋ฌธ์ œ ํšŒํ”ผํ•˜๊ธฐ

    ์‚ฌ์‹ค ํ•„์ž๋„ ์ฒ˜์Œ์—๋Š” ๋กœ์ปฌ์— HTTPS ๋ถ™ํžˆ๊ธฐ๊ฐ€ ๊ท€์ฐฎ์•„์„œ ๋ฆฌ์„œ์น˜ํ•˜๋‹ค๊ฐ€ ์ฐพ์€ ์–Œ์ƒ์ด์ธ๋ฐ, ๋กœ์ปฌ ์„œ๋ฒ„์—์„œ๋„ HTTPS๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ ๋„ ์„œ๋น„์Šค ์›Œ์ปค๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค.

    chrome://flags/#unsafely-treat-insecure-origin-as-secure์— ๋“ค์–ด๊ฐ€์„œ ํ•ด๋‹น ๊ธฐ๋Šฅ์„ ํ™œ์„ฑํ™”ํ•˜๊ณ  ํ…์ŠคํŠธ ํ•„๋“œ์— ์›ํ•˜๋Š” ํ˜ธ์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•ด๋†“์œผ๋ฉด ํ•ด๋‹น ํ˜ธ์ŠคํŠธ๋Š” ์•ˆ์ „ํ•˜๋‹ค๊ณ  ํŒ๋‹จํ•˜์—ฌ ์„œ๋น„์Šค ์›Œ์ปค๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค€๋‹ค.

    secure hack

    ์ด๋ ‡๊ฒŒ ๋“ฑ๋กํ•œ ๋’ค ํ•˜๋‹จ์˜ Relaunch Now ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ํฌ๋กฌ์ด ์žฌ์‹œ์ž‘๋˜๋ฉด์„œ ์„ค์ •์ด ๋ณ€๊ฒฝ๋œ๋‹ค. ํ•˜์ง€๋งŒ ์ด ๋ฐฉ๋ฒ•์€ ์Šค์Šค๋กœ ๋ณด์•ˆ ์ทจ์•ฝ์ ์„ ๋งŒ๋“ค์–ด ๋‚ด๋Š” ๊ฒƒ์ด๊ธฐ ์ฐœ์ฐœํ•ด์„œ ๊ฒฐ๊ตญ ์‚ฌ์šฉํ•˜์ง€๋Š” ์•Š์•˜๋‹ค.

    Service Worker ์ž‘์„ฑ

    ์ด์ œ ๊ฐœ๋ฐœํ™˜๊ฒฝ ์„ธํŒ…์ด ๋‹ค ๋๋‚ฌ๋‹ค๋ฉด ์ด์ œ๋ถ€ํ„ฐ๋Š” ๋”ฑํžˆ ๊ท€์ฐฎ์€ ๊ฑด ์—†๋‹ค. ๊ทธ๋ƒฅ ๋ฌธ์„œ๋ณด๋ฉด์„œ ์ญ‰์ญ‰ ์ž‘์„ฑํ•˜๋ฉด ๋œ๋‹ค. ์ผ๋‹จ ์„œ๋น„์Šค ์›Œ์ปค์˜ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์ „์— ์ž˜ ์ž‘๋™ํ•˜๋Š” ์ง€ ๋ถ€ํ„ฐ ํ™•์ธํ•ด์•ผ ํ•˜๋ฏ€๋กœ ์•„๋ฌด ๋‚ด์šฉ์ด ์—†๋Š” ์„œ๋น„์Šค ์›Œ์ปค๋ถ€ํ„ฐ ๋งŒ๋“ค์—ˆ๋‹ค.

    ๊ทผ๋ฐ ์ด๊ฒƒ๋„ manifest.json์ฒ˜๋Ÿผ ๊ทธ๋ƒฅ ์›น ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ ์™„์ „ ๋ถ„๋ฆฌ๋œ static/service-worker.js๋กœ ๋”ฐ๋กœ ์ž‘์„ฑํ•˜๋ฉด ๋งŒ๋“ค ๋•Œ๋Š” ํŽธํ•˜๊ธด ํ•œ๋ฐ, ๋‚˜์ค‘์— ์„œ๋น„์Šค ์›Œ์ปค์—์„œ ์›น ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” ๋ชจ๋“ˆ์ด๋‚˜ ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•˜๊ณ  ์‹ถ์„ ๋•Œ ๊ต‰์žฅํžˆ ์• ๋งคํ•ด์งˆ ๊ฒƒ ๊ฐ™์•„์„œ ๊ทธ๋ƒฅ Webpack์„ ์‚ฌ์šฉํ•ด์„œ ๊ฐ™์ด ๋นŒ๋“œํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ๋‹ค.

    ์ผ๋‹จ service-worker.js ํŒŒ์ผ์„ ๋งŒ๋“ค์ž!

    Webpack์œผ๋กœ ๋นŒ๋“œํ•œ๋‹ค๊ณ  ํ•ด๋„ ์ด ์นœ๊ตฌ๊ฐ€ ๋ฌด์—์„œ ์œ ๋ฅผ ์ฐฝ์กฐํ•˜๋Š” ์นœ๊ตฌ๋Š” ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋‹น์—ฐํžˆ ์†Œ์Šค ํŒŒ์ผ์€ ์žˆ์–ด์•ผ ํ•œ๋‹ค. ์ผ๋‹จ ์„œ๋น„์Šค ์›Œ์ปค๊ฐ€ ์ž‘๋™ํ•˜๋Š” ๊ฒƒ์„ ๋ณด๋Š” ๊ฒƒ์ด ๋ชฉ์ ์ด๋ฏ€๋กœ ์‹ฌํ”Œํ•˜๊ฒŒ ์ž‘์„ฑํ•ด์ฃผ์—ˆ๋‹ค.

    // src/service-worker.js
    self.addEventListener('message', event => {
      console.log('์ € ์ชฝ ํ…Œ์ด๋ธ”์—์„œ ๋ณด๋‚ด์‹  ๊ฒ๋‹ˆ๋‹ค -> ', event);
    });

    Service Worker Webpack Plugin ์‚ฌ์šฉํ•˜๊ธฐ

    ServiceWorkerWebpackPlugin์€ ๊ตฌ๊ธ€์— Service Worker Webpack์„ ๊ฒ€์ƒ‰ํ•˜๋ฉด ๊ฐ€์žฅ ์ƒ๋‹จ์— ๋‚˜์˜ค๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ์ด๋‹ค. ๊นƒํ—ˆ๋ธŒ ๋ ˆํŒŒ์ง€ํ† ๋ฆฌ์— ๊ฐ€์„œ ๋‹ค๋“ค ๊ตฌ๊ฒฝ ํ•œ๋ฒˆ ํ•ด๋ณด์ž.

    README๋ฅผ ์ฝ์–ด๋ณด๋‹ˆ ์‚ฌ์šฉ๋ฒ•์ด ์ดˆ๊ฐ„๋‹จ ๊ทธ ์ž์ฒด๋‹ค. ๊ทธ๋ƒฅ Webpack ์„ค์ •์— ๋„ฃ์–ด์ฃผ๋ฉด ๋์ด๋‹ค.

    // build/webpack.client.config.js
    module.exports = merge(baseConfig, {
      plugins: [
        // ...
        new ServiceWorkerWebpackPlugin({
            entry: path.resolve(__dirname, '../src/service-worker.js'),
        }),
        // ...
      ]
    })

    ์„œ๋น„์Šค ์›Œ์ปค ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ์ฐ์–ด์ฃผ๊ณ  ServiceWorkerWebpackPlugin ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•  ๋•Œ ์ดˆ๊ธฐ ์ธ์ž๋กœ ๋„˜๊ฒจ์ฃผ๋ฉด Webpack ์„ค์ •์˜ output์— ์ •์˜๋œ ๊ฒฝ๋กœ์— sw.js ํŒŒ์ผ์„ ์ƒ์„ฑํ•ด์ค€๋‹ค. ๋ฌผ๋ก  ์ด ํŒŒ์ผ ์ด๋ฆ„์€ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ ๋ชจ๋‘ ๊ฐ์ž์˜ ์ทจํ–ฅ๋Œ€๋กœ ์• ์ •์„ ๋“ฌ๋ฟ ๋‹ด์€ ์ด์œ ์ด๋ฆ„์„ ์ง€์–ด์ฃผ์ž. ํ•„์ž๋Š” ๊ท€์ฐฎ์œผ๋‹ˆ๊นŒ ๊ทธ๋ƒฅ sw๋กœ ๊ฐ€๊ธฐ๋กœ ํ–ˆ๋‹ค.

    sw 이쁘게 태어난 sw.js

    ๊ทผ๋ฐ ์ด ๋ฐฉ๋ฒ•์˜ ๋‹จ์ ์ด ์žˆ๋Š”๋ฐ, ์ผ๋ฐ˜์ ์ธ ์„œ๋น„์Šค ์›Œ์ปค๋ณด๋‹ค ํŒŒ์ผ์˜ ํฌ๊ธฐ๊ฐ€ ์ปค์ง„๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ์•„๋ฌด๋ž˜๋„ Webpack์œผ๋กœ ๋นŒ๋“œํ•˜๋‹ค๋ณด๋‹ˆ ์„œ๋น„์Šค ์›Œ์ปค์˜ ์™ธ๋ถ€์— ์žˆ๋Š” ๋ชจ๋“ˆ์ด๋‚˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋Œ์–ด์˜ค๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๋ญ ์ด๊ฑด ์• ์ดˆ์— ํ•„์ž๊ฐ€ ์กฐ๊ธˆ ํŽธํ•˜๊ฒŒ ์“ฐ๋ ค๊ณ  ์„ ํƒํ•œ ์ƒํ™ฉ์ด๋‹ˆ ์–ด์ฉ” ์ˆ˜ ์—†๊ธดํ•˜๋‹ค. ์–ด์จŒ๋“  ์ด๋Ÿฐ ๋‹จ์ ์ด ์žˆ์œผ๋‹ˆ, ์„œ๋น„์Šค ์›Œ์ปค์˜ ํฌ๊ธฐ๋ฅผ ์ค„์ด๊ณ ์ž ํ•˜์‹œ๋Š” ๋…์ž๋ถ„๋“ค์€ ์ง์ ‘ ์ž‘์„ฑํ•˜์‹œ๋Š” ๊ฒŒ ๋” ์ข‹์„ ์ˆ˜๋„ ์žˆ๋‹ค.

    ์ด์ œ ์„œ๋น„์Šค ์›Œ์ปค์˜ ๋ณธ์ฒด๋ฅผ ๋งŒ๋“ค์—ˆ์œผ๋‹ˆ ์ด๊ฑธ ์›น ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ดˆ๊ธฐํ™”๋  ๋•Œ ๋ธŒ๋ผ์šฐ์ €์— ๋‚˜ ์„œ๋น„์Šค ์›Œ์ปค ๊ฐ€์ง€๊ณ  ์žˆ์–ด!๋ผ๊ณ  ์•Œ๋ ค์ฃผ๋Š” ์ผ๋งŒ ๋‚จ์•˜๋‹ค.

    Service Worker ์„ค์น˜

    ์„œ๋น„์Šค ์›Œ์ปค๋ฅผ ์„ค์น˜ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๊ฐ„๋‹จํ•˜๋‹ค. ์ด ๋ธŒ๋ผ์šฐ์ €์— serviceWorker ๊ฐ์ฒด๊ฐ€ ์ง€์›๋˜๋Š” ์ง€ ํ™•์ธํ•œ ํ›„ ์„ค์น˜ํ•˜๋ฉด ๋œ๋‹ค.

    ์ผ๋ฐ˜์ ์ธ ์„œ๋น„์Šค ์›Œ์ปค์˜ ์„ค์น˜ ๋ฐฉ๋ฒ•์€ ๊ตฌ๊ธ€์˜ ์„œ๋น„์Šค ์›Œ์ปค:์†Œ๊ฐœ ๋ฌธ์„œ๋ฅผ ๋ณด๊ณ  ๋”ฐ๋ผํ•˜๋ฉด ๋œ๋‹ค. ํ•„์ž๋Š” ServiceWorkerWebpackPlugin์„ ์‚ฌ์šฉํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ํ•ด๋‹น ํ”Œ๋Ÿฌ๊ทธ์ธ์˜ ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์—ฌ ์ž‘์„ฑํ–ˆ๋‹ค.

    ์–ด์ฐจํ”ผ ์–ด๋–ค ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•˜๋“  ์„ค์น˜ ์ž์ฒด๋Š” ๊ทธ๋ ‡๊ฒŒ ์–ด๋ ต์ง€ ์•Š๋‹ค.

    // settings/service-worker.setting.js
    export default () => {
      const isSupported = process.browser && 'serviceWorker' in navigator;
    
      if (!isSupported) {
        return;
      }
    
      console.log('์„œ๋น„์Šค ์›Œ์ปค๊ฐ€ ์ง€์›๋˜๋Š” ๋ธŒ๋ผ์šฐ์ € ์ž…๋‹ˆ๋‹ค.');
    
      const runtime = require('serviceworker-webpack-plugin/lib/runtime');
    
      runtime.register().then(res => {
        console.log('์„œ๋น„์Šค ์›Œ์ปค ์„ค์น˜ ์„ฑ๊ณต ->', res);
      }).catch(e => {
        console.log('์„œ๋น„์Šค ์›Œ์ปค ์„ค์น˜ ์‹คํŒจ ใ…œใ…œ -> ', e);
      });
    }

    isSupported ๋ณ€์ˆ˜์— process.browser ๊ฐ’์„ ๊ฒ€์‚ฌํ•˜๋Š” ์ด์œ ๋Š”, ์ˆจ๊ณ ์˜ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์€ Next.js๋‚˜ Nuxt.js ์ฒ˜๋Ÿผ ์œ ๋‹ˆ๋ฒ„์…œ SSR ํ™˜๊ฒฝ์—์„œ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์œ ๋‹ˆ๋ฒ„์…œ SSR์ด ๋ญ”์ง€ ๊ถ๊ธˆํ•˜์‹  ๋ถ„์€ ์ด์ „์— ์ž‘์„ฑํ•œ ํฌ์ŠคํŒ…์ธ Universal Server Side Rendering์ด๋ž€?์„ ํ•œ๋ฒˆ ์ฝ์–ด๋ณด์ž.

    ํ•ด๋‹น ๋ชจ๋“ˆ์€ ๋ฌผ๋ก  ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ์—์„œ๋งŒ ํ˜ธ์ถœ๋˜์ง€๋งŒ ๊ทธ๋ž˜๋„ ํ˜น์‹œ ๋ชจ๋ฅด๋‹ˆ ์™ ๋งŒํ•˜๋ฉด ํ˜„์žฌ ์‹คํ–‰ ์ปจํ…์ŠคํŠธ๊ฐ€ ํด๋ผ์ด์–ธํŠธ์ธ์ง€ ์„œ๋ฒ„์ธ์ง€๋Š” ์ฒดํฌํ•ด์ฃผ๋Š” ๊ฒƒ์ด ์ข‹๋‹ค. ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง ์‚ฌ์ดํด ๋•Œ ๋ธŒ๋ผ์šฐ์ € API์ธ navigator์— ์ ‘๊ทผํ•˜๋ ค๊ณ  ํ•˜๋ฉด ๋‹น์—ฐํžˆ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

    ์„œ๋น„์Šค ์›Œ์ปค์˜ ์„ค์น˜๊ฐ€ ์„ฑ๊ณตํ–ˆ๋‹ค๋ฉด chrome://inspect/#service-workers ๋˜๋Š” chrome://serviceworker-internals์— ์—ฌ๋Ÿฌ๋ถ„์˜ ์„œ๋น„์Šค ์›Œ์ปค๊ฐ€ ๋…ธ์ถœ๋  ๊ฒƒ์ด๋‹ค. ์„œ๋น„์Šค ์›Œ์ปค๋ฅผ ์ ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์‚ฌ์‹ค ๊ต‰์žฅํžˆ ์‹ฌํ”Œํ•œ ํŽธ์ด๋‹ค.

    ๊ทผ๋ฐ ์‚ฌ์‹ค ํ•„์ž๊ฐ€ ๊ฐ€์žฅ ์‹œ๊ฐ„์„ ๋งŽ์ด ์žก์•„๋จน์€ ๋ถ€๋ถ„์ด ๋ฐ”๋กœ ์ด ์„œ๋น„์Šค ์›Œ์ปค์˜€๋Š”๋ฐ, ๊ทธ ์ด์œ ๋Š” ์„œ๋น„์Šค ์›Œ์ปค ๊ณต์‹ ๋ฌธ์„œ์— ์ ํ˜€์žˆ๋‹ค.

    ์„ค์น˜ ์‹คํŒจ ์•Œ๋ฆผ ๊ธฐ๋Šฅ ๋ถ€์กฑ

    ์„œ๋น„์Šค ์›Œ์ปค๊ฐ€ ๋“ฑ๋ก๋˜๋”๋ผ๋„ chrome://inspect/#service-workers ๋˜๋Š” chrome://serviceworker-internals์— ํ‘œ์‹œ๋˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๊ฑฐ๋‚˜ event.waitUntil()์— ๊ฑฐ๋ถ€๋œ ํ”„๋ผ๋ฏธ์Šค๋ฅผ ์ „๋‹ฌํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์„ค์น˜ํ•˜์ง€ ๋ชปํ–ˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋ ค๋ฉด chrome://serviceworker-internals๋กœ ์ด๋™ํ•˜์—ฌ โ€˜Open DevTools window and pause JavaScript execution on service worker startup for debuggingโ€™์„ ์„ ํƒํ•˜๊ณ  ์„ค์น˜ ์ด๋ฒคํŠธ์˜ ์‹œ์ž‘ ์œ„์น˜์— ๋””๋ฒ„๊ฑฐ ๋ฌธ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ์ด ์˜ต์…˜์„ ํ™•์ธํ•  ์ˆ˜ ์—†๋Š” ์˜ˆ์™ธ ์‹œ ์ผ์‹œ ์ค‘์ง€์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ๋ฌธ์ œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    Matt Gaunt contributor to WebFundamentals

    ์•„๋‹ˆ ์ด๊ฒŒ ๋””๋ฒ„๊น…์ด ์ง„์งœ ํž˜๋“ค๋‹ค. ์„œ๋น„์Šค ์›Œ์ปค์˜ ์„ค์น˜๊ฐ€ ์‹คํŒจํ–ˆ์œผ๋ฉด ์–ด๋””์„œ ์—๋Ÿฌ๊ฐ€ ๋‚ฌ๋Š”์ง€, ์™œ ๋‚ฌ๋Š”์ง€ ๋ณด์—ฌ์ค˜์•ผ ํ•˜๋Š”๋ฐ Uncaught DomException ํ•˜๋‚˜๋งŒ ๋”ธ๋ž‘ ๋ณด์—ฌ์ฃผ๊ณ  ๋๋‚ธ๋‹ค. ๊ทธ๋ž˜์„œ ๋ญ๊ฐ€ ์ž˜๋ชป๋˜์—ˆ๋Š”์ง€ ํ•˜๋‚˜ํ•˜๋‚˜ ์ฝ”๋“œ ๋ผ์ธ์— debugger ์ฐ๊ณ  ์ถ”๋ฆฌํ•ด๊ฐ€๋ฉด์„œ ์ˆ˜์ •ํ•ด์•ผํ•˜๋Š”๋ฐ ์ด๊ฒŒ ์ง„์งœ ํž˜๋“ค๋‹ค. ๊ทผ๋ฐ ์ด๊ฑธ ๋˜ ๊ณต์‹ ๋ฌธ์„œ์— ๋””๋ฒ„๊น… ํž˜๋“ค๋‹ค๊ณ  ๋ฒ„์ “์ด ์ ์–ด๋†”์„œ ๊ดœํžˆ ๋” ํž˜๋“  ๊ฑฐ ๊ฐ™๋‹ค.

    conan 도와줘요 명탐정... 범인말고 내 버그도 찾아줘...

    ์ œ 3 ๊ด€๋ฌธ, PushManager

    ์ด์ œ ์„œ๋น„์Šค ์›Œ์ปค๋„ ์„ค์น˜ํ–ˆ์œผ๋‹ˆ ์„œ๋น„์Šค ์›Œ์ปค์˜ PushManager๋งŒ ์—ฐ๋™ํ•ด์ฃผ๋ฉด ๋ชจ๋“  ๊ฒƒ์ด ๋๋‚œ๋‹ค! ๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์ง€๋งŒ ์ด ๋†ˆ๋„ ๋ณต๋ณ‘์ด์—ˆ๋‹ค. PushManager ๋˜ํ•œ ์•„์ง ํ‘œ์ค€์ด ์•„๋‹ˆ๋ผ์„œ ๋ชจ๋“  ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ง€์›๋˜๋Š” ๊ธฐ๋Šฅ์ด ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ์„œ๋น„์Šค ์›Œ์ปค ์„ค์น˜ ์‹œ PushManager์˜ ์กด์žฌ ์—ฌ๋ถ€๋„ ํ•จ๊ป˜ ๊ฒ€์‚ฌํ•ด์ฃผ์–ด์•ผํ•œ๋‹ค.

    // settings/service-worker.setting.js
    const isSupported = process.browser && 'serviceWorker' in navigator && 'PushManager' in window;
    // ...

    ์กฐ๊ฑด์˜ ๊ฐ€๋…์„ฑ์ด ์กฐ๊ธˆ ๋–จ์–ด์ง„ ๊ฒƒ์ด ๋งˆ์Œ์— ์•ˆ๋“ค์ง€๋งŒ ์ผ๋‹จ์€ ๊ตฌ๋™ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋Š” ๊ฒƒ์ด๋ฏ€๋กœ ๊ทธ๋ƒฅ ๋„˜์–ด๊ฐ”๋‹ค. ๋”ฑ ์—ฌ๊ธฐ๊นŒ์ง€ ํ•˜๊ณ ๋‚˜์„œ ๋‹ค์Œ ์Šคํ…์„ ๋ดค๋”๋‹ˆโ€ฆ

    ์žฌ์•™์˜ ์‹œ์ž‘

    ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋ฒ„ ํ‚ค ๊ฐ€์ ธ์˜ค๊ธฐ

    ์ด ์ฝ”๋“œ๋žฉ์œผ๋กœ ์ž‘์—…ํ•˜๋ ค๋ฉด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋ฒ„ ํ‚ค๋ฅผ ๋ช‡ ๊ฐœ ์ƒ์„ฑํ•  ํ•„์š”๊ฐ€ ์žˆ๋Š”๋ฐ, ๋„์šฐ๋ฏธ ์‚ฌ์ดํŠธ์ธ https://web-push-codelab.glitch.me/์—์„œ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    ์—ฌ๊ธฐ์„œ ๊ณต๊ฐœ ํ‚ค ์Œ๊ณผ ๋น„๊ณต๊ฐœ ํ‚ค ์Œ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    ๋‹ค์Œ๊ณผ ๊ฐ™์ด scripts/main.js๋กœ ๊ณต๊ฐœ ํ‚ค๋ฅผ ๋ณต์‚ฌํ•˜์—ฌ <Your Public Key> ๊ฐ’์„ ๋ฐ”๊พธ์„ธ์š”.

    const applicationServerPublicKey = '<Your Public Key>';

    ์ฐธ๊ณ : ์ ˆ๋Œ€๋กœ ๋น„๊ณต๊ฐœ ํ‚ค๋ฅผ ์›น ์•ฑ์— ๋‘๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค!

    Matt Gaunt ์›น ์•ฑ์— ํ‘ธ์‹œ ์•Œ๋ฆผ ์ถ”๊ฐ€

    ์‘โ€ฆ? SSH ํ‚ค๊ฐ€ ํ•„์š”ํ•˜๋‹ค๊ณ โ€ฆ? ์˜ˆ์ƒ ๋ชปํ•˜๊ธด ํ–ˆ์ง€๋งŒ HTTPS ํ™˜๊ฒฝ์—์„œ ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ ํ†ต์‹  ์ฑ„๋„์ด ํ•˜๋‚˜ ๋” ์ƒ๊ธฐ๋Š” ๊ฒƒ์ด๋ฏ€๋กœ SSH ํ‚ค๊ฐ€ ํ•„์š”ํ•œ ๊ฑด ๋งž๋Š” ๊ฒƒ ๊ฐ™์•„์„œ ๋น ๋ฅด๊ฒŒ ์ธ์ •ํ–ˆ๋‹ค.

    Google์˜ ๊ณต์‹ ๋ฌธ์„œ์—์„œ PushManager๋กœ ํ‘ธ์‹œ ์ฑ„๋„์„ ๊ตฌ๋…ํ•˜๋Š” ์˜ˆ์ œ๋ฅผ ์‚ดํŽด๋ดค๋”๋‹ˆ ํ™•์‹คํžˆ SSH ํ‚ค๊ฐ€ ํ•„์š”ํ•˜๊ธด ํ–ˆ๋‹ค.

    function subscribeUser() {
      const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
      swRegistration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: applicationServerKey
      })
      // ...
    }

    ์Œ ์˜ˆ์ƒ์€ ๋ชปํ–ˆ์ง€๋งŒ ์ด ์ •๋„๋Š” ๊ดœ์ฐฎ๋‹ค. ์–ด์ฐจํ”ผ Pusher๋ผ๋Š” ํ‘ธ์‹œ ์†”๋ฃจ์…˜์„ ์ด๋ฏธ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๊ณ  ์ด๊ฒƒ๋„ ๊ฒฐ๊ตญ ๊ฐ™์€ ์›๋ฆฌ๋กœ ์ž‘๋™ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋‚ด๋ถ€์ ์œผ๋กœ๋Š” SSH ํ‚ค์Œ์„ ์‚ฌ์šฉํ•œ ์ธ์ฆ์„ ์‚ฌ์šฉํ–ˆ์„ ๊ฒƒ์ด๋‹ค.

    ๋น ๋ฅธ ์†์ ˆ

    ๊ทธ๋ ‡๊ฒŒ Pusher์—์„œ ์‚ฌ์šฉ๋˜๋Š” SSH ํ‚ค๊ฐ€ ์–ด๋”” ์žˆ๋Š”์ง€ ์ฐพ๊ธฐ๋ฅผ ์–ด์–ธ 20๋ถ„โ€ฆ ๊ฒฐ๊ตญ ๋ชป ์ฐพ์•˜๋‹ค.

    Pusher ๋‚ด๋ถ€์ ์œผ๋กœ ์„œ๋ฒ„์—์„œ ์‚ฌ์šฉํ•˜๋Š” secret๊ฐ’๊ณผ ํด๋ผ์ด์–ธํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋Š” key๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ธ์ฆ์„ ํ•˜๋Š”๋ฐ, ์ด๊ฑด SSH ํ‚ค์Œ์ด ์•„๋‹ˆ๋ผ ๊ทธ๋ƒฅ ์ž„์˜์˜ ๋ฌธ์ž์—ด์ด์—ˆ๋‹ค. ์–ด์ฐจํ”ผ SSH ํ‚ค๋ฅผ ๋งŒ๋“ ๋‹ค๊ณ  ํ•ด๋„ ์›น ํ‘ธ์‹œ์— ๋Œ€ํ•œ ์ปจํŠธ๋กค์€ ๋ฐฑ์—”๋“œ๊ฐ€ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฏ€๋กœ ํ•„์ž ํ˜ผ์ž ์ด๊ฒƒ์ €๊ฒƒ ๊ฑด๋“œ๋ฆฌ๋ฉด์„œ ํ…Œ์ŠคํŠธ ํ•ด๋ณด๊ธฐ์—๋Š” ์กฐ๊ธˆ ๋ฌด๋ฆฌ๊ฐ€ ์žˆ๋‹ค. ๊ฒŒ๋‹ค๊ฐ€ ํ‡ด๊ทผ ์‹œ๊ฐ„์€ ์ด๋ฏธ ํ•œ์ฐธ ์ง€๋‚ฌ๊ธฐ ๋•Œ๋ฌธ์— ๋ฐฑ์—”๋“œ ๋ถ„๋“ค์€ ํ‡ด๊ทผํ•˜์…จ๋‹ค.(์‚ฌ์‹ค ๊ธˆ์š”์ผ ์ €๋…์— ์ด๋Ÿฐ ๊ฑฐ ํ•˜๊ณ  ์žˆ๋Š” ์‚ฌ๋žŒ์ด ์ด์ƒํ•œ๊ฑฐ๋‹ค.)

    ์ด๋•Œ ์ด๋ฏธ ํ•„์ž์˜ ๋ฉ˜ํƒˆ์€ ์กฐ๊ธˆ ๋‚˜๊ฐ€์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์›”์š”์ผ์— ์ถœ๊ทผํ•ด์„œ ๋ชจ๋ฐ”์ผ ์•ฑ์—์„œ๋Š” ์–ด๋–ป๊ฒŒ Pusher์™€ FCM์„ ์—ฐ๋™ํ•ด์„œ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” ์ง€ ๋ฌผ์–ด๋ณธ ํ›„ ์ง„ํ–‰ํ•ด์•ผ๊ฒ ๋‹ค๊ณ  ๊ฒฐ๋ก ์„ ๋‚ด๋ฆฌ๊ณ  ๋ฐฉํ–ฅ์„ ๋ฐ”๊ฟจ๋‹ค.

    ์ปค๋ฐ‹ ๋กœ๊ทธ๋ฅผ ๋ณด๋‹ˆ ์ด๋•Œ ์‹œ๊ฐ„์ด ๋Œ€๋žต 21์‹œ 40๋ถ„ ์ฏคโ€ฆ ์•„๋‹ˆ ๊ทธ๋ž˜๋„ ํ‡ด๊ทผ์€ ํ•ด์•ผํ•˜๋‹ˆ๊นŒโ€ฆ ์ง‘์—๋Š” ๊ฐ€์•ผ์ง€โ€ฆ

    giveup 역시 아니다 싶으면 빠른 손절이 답이다...

    ์ œ 4 ๊ด€๋ฌธ, Notification API

    ๊ทธ๋ž˜์„œ ๋ฐ”๊พผ ๋ฐฉํ–ฅ์€ โ€œBackground ํ‘ธ์‹œ ๋ฉ”์„ธ์ง€๋Š” ํฌ๊ธฐํ•˜๊ณ  Foreground ํ‘ธ์‹œ ๋ฉ”์„ธ์ง€๋ผ๋„ ์ œ๋Œ€๋กœ ๋ฐ›๊ฒŒ ํ•˜์žโ€์˜€๋‹ค.

    ์ด๋ ‡๊ฒŒ ๋˜๋ฉด ํ•„์ž๊ฐ€ ์ฒ˜์Œ ์ƒ๊ฐํ–ˆ๋˜ โ€œ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๊บผ์ ธ์žˆ๋”๋ผ๋„ ํ‘ธ์‹œ ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ  ์‹ถ๋‹คโ€๋Š” ๋‹ฌ์„ฑํ•˜์ง€ ๋ชปํ•˜์ง€๋งŒ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ผœ์ ธ์žˆ๊ณ  soomgo.com์— ์ ‘์†๋˜์–ด ์žˆ๋‹ค๋ฉด ์ฐฝ์„ ๋‚ด๋ ค๋†“๋“  ๋‹ค๋ฅธ ์ผ์„ ํ•˜๊ณ  ์žˆ๋“  ์‚ฌ์šฉ์ž์—๊ฒŒ ํ‘ธ์‹œ ๋ฉ”์„ธ์ง€๋Š” ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์–ด๋А ์ •๋„ ๋ชฉ์  ๋‹ฌ์„ฑ์€ ๋œ๋‹ค.

    ๋ฐฉํ–ฅ์„ ๋ฐ”๊พธ๊ณ  ๋‚˜๋‹ˆ๊นŒ ๊ธฐ์กด์˜ ์›น ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๊ตฌํ˜„๋˜์–ด์žˆ๋˜ ํ‘ธ์‹œ ๋กœ์ง์— ๋…ธํ‹ฐํ”ผ์ผ€์ด์…˜์„ ๋ณด์—ฌ์ฃผ๋Š” ์ฝ”๋“œ๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด ๋๋‚˜๋Š” ๊ฐ„๋‹จํ•œ ์ผ์ด ๋˜์—ˆ๋‹ค. ์–ด์ฐจํ”ผ Pusher ์†”๋ฃจ์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ์ธ์ฆ, ์ด๋ฒคํŠธ ๊ตฌ๋… ๋“ฑ์˜ ๋กœ์ง์€ ์˜ˆ์ „์— ์ฑ„ํŒ… ๊ธฐ๋Šฅ ๊ฐœ๋ฐœํ•  ๋•Œ ๋‹ค ๋งŒ๋“ค์–ด ๋†จ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

    ๊ธฐ์กด ๊ธฐ๋Šฅ์— Notification ๋ผ์›Œ๋„ฃ๊ธฐ

    ์˜ˆ์ „์— ์ฑ„ํŒ… ๊ธฐ๋Šฅ์„ ๊ฐœ๋ฐœํ•  ๋•Œ Pusher SDK๋ฅผ ํ•œ๋ฒˆ ๋ž˜ํ•‘ํ•œ ํ—ฌํผ ํด๋ž˜์Šค๋„ ๋งŒ๋“ค์–ด ๋†จ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋‚˜๋ฆ„ ๊ตฌ์กฐ๋„ ํƒ„ํƒ„ํ•˜๋‹ค. ์ด์ œ ์—ฌ๊ธฐ์— ๋ฉ”์†Œ๋“œ๋งŒ ๋ช‡๊ฐœ ์ถ”๊ฐ€ํ•˜๊ณ  ์›น ํ‘ธ์‹œ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์•Œ๋ฆผ๋งŒ ๋ณด์—ฌ์ฃผ๋ฉด ๋œ๋‹ค.

    ์šฐ์„  ์ด ๋ธŒ๋ผ์šฐ์ €๊ฐ€ Notification API๋ฅผ ์ง€์›ํ•˜๋Š” ์ง€ ํ™•์ธํ•˜๋Š” ๋ฉ”์†Œ๋“œ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

    // src/helpers/Pusher.js
    isSupportNotification () {
      return process.browser && window && 'Notification' in window;
    }

    ๊ทธ ๋‹ค์Œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ์— ๋Œ€ํ•œ ํ—ˆ๊ฐ€๋ฅผ ๋ฐ›๋Š” ๋ฉ”์†Œ๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค. Notification์€ ๋‚ด๋ถ€์— permission ์†์„ฑ์„ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ  ์ด ์†์„ฑ์€ granted, denied, default๋กœ ๋‚˜๋ˆ„์–ด ์ง„๋‹ค.

    // src/helpers/Pusher.js
    getNotificationPermission () {
      if (!this.isSupportNotification()) {
        this.isAllowNotification = false;
        return Promise.reject(new Error('not_supported'));
      }
    
      if (Notification.permission === 'granted') {
        this.isAllowNotification = true;
        return Promise.resolve();
      }
      else if (Notification.permission !== 'denied' || Notification.permission === 'default') {
        return Notification.requestPermission().then(result => {
          if (result === 'granted') {
            this.isAllowNotification = true;
          }
        });
      }
    }

    granted๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋ฏธ ์•Œ๋ฆผ์„ ํ—ˆ์šฉํ•œ ์ƒํƒœ, denied๋Š” ๊ฑฐ๋ถ€ํ•œ ์ƒํƒœ, default๋Š” ์•„์ง ์•Œ๋ฆผ์— ๋Œ€ํ•œ ํผ๋ฏธ์…˜์„ ์ค„์ง€๋ง์ง€ ์‚ฌ์šฉ์ž๊ฐ€ ๊ฒฐ์ •์„ ํ•˜์ง€ ์•Š์€ ์ƒํƒœ์ด๋‹ค. ๋”ฐ๋ผ์„œ ์šฐ๋ฆฌ๋Š” ํผ๋ฏธ์…˜์ด default ์ƒํƒœ์ผ ๋•Œ Notification.requestPermission ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ ๋…ธ์ถœ์— ๋Œ€ํ•œ ํ—ˆ๊ฐ€๋ฅผ ๋ฐ›์•„์•ผ ํ•œ๋‹ค.

    ์ด์ œ ์‹ค์ œ๋กœ ์•Œ๋ฆผ์ฐฝ์„ ๋„์›Œ์ค„ ๋ฉ”์†Œ๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž. Notification API ์ž์ฒด๊ฐ€ ์›Œ๋‚™ ์‹ฌํ”Œํ•˜๋‹ค๋ณด๋‹ˆ ๊ทธ๋‹ฅ ์–ด๋ ต์ง€ ์•Š๋‹ค.

    createForegroundNotification (title, { body, icon, link }) {
      const notification = new Notification(title, {
        body,
        icon: icon || `${AssetsCloudFrontHost}/app_icons/1x.png`,
      });
    
      notification.onshow = () => {
        setTimeout(() => notification.close(), 5000);
      };
      notification.onerror = e => {
        console.error(e);
      };
      notification.onclick = event => {
        event.preventDefault();
        if (link) {
            window.open(link, '_blank');
        }
      };
    }

    new ํ‚ค์›Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Notification ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ๊ทธ ์ฆ‰์‹œ OSX๋Š” ํ™”๋ฉด ์šฐ์ธก ์ƒ๋‹จ์—, Windows๋Š” ์šฐ์ธก ํ•˜๋‹จ์— ์•Œ๋ฆผ ๋ฉ”์„ธ์ง€๊ฐ€ ๋…ธ์ถœ๋œ๋‹ค. ๊ทธ ๋‹ค์Œ ์ƒ์„ฑํ•œ Notification๊ฐ์ฒด์˜ onshow, onclick ๋“ฑ์˜ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ์— ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๋“ฑ๋กํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

    ๋ฉ”์†Œ๋“œ ๋ช…์€ Background ๋ฉ”์„ธ์ง•์„ ์‹คํ˜„ํ•˜์ง€ ๋ชปํ•œ ํ•„์ž์˜ ์Šฌํ””์„ ๋‹ด์•„ createForegroundNotification์œผ๋กœ ๊ฒฐ์ •ํ–ˆ๋‹ค. ๊ตณ์ด Foreground๋ฅผ ๊ฐ•์กฐํ•œ ์ด์œ ๋Š” ์–ธ์  ๊ฐ€ createBackgroundNotification ๋ฉ”์†Œ๋“œ๋ฅผ ๋งŒ๋“ค๊ฒ ๋‹ค๋Š” ํ•„์ž์˜ ์•ผ๋ง์„ ๋‹ด์•˜๋‹ค.

    ์ด์ œ ํ•„์š”ํ•œ ๋ชจ๋“  ๊ฒƒ์„ ๋งŒ๋“ค์—ˆ์œผ๋‹ˆ Pusher SDK์—์„œ Web Socket์„ ํ†ตํ•ด ํ‘ธ์‹œ๋ฅผ ๋ณด๋‚ผ ๋•Œ๋งˆ๋‹ค ์•Œ๋ฆผ์ด ์ž‘๋™ํ•˜๋„๋ก ์—ฐ๊ฒฐ๋งŒ ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

    async subscribeNotification () {
      if (!this.isSupportNotification()) {
        return;
      }
    
      await this.getNotificationPermission();
      if (!this.isAllowNotification) {
        return;
      }
    
      const channel = await this.getPrivateUserChannel();
      channel.bind('message', response => {
        if (response.sender.id === this.myUserId) {
          return;
        }
    
        const targetChatRoute = !!response.sender.provider ? 'chats' : 'pro/chats';
        this.createForegroundNotification(`${response.sender.name}๋‹˜์ด ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด๋ƒˆ์–ด์š”.`, {
          body: response.message,
          icon: response.sender.profile_image,
          link: `${location.origin}/${targetChatRoute}/${response.chat.id}`,
        });
      });
    }

    Pusher SDK๋Š” ํ‘ธ์‹œ ์ฑ„๋„์— ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๋ฐ”์ธ๋”ฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด์ค€๋‹ค. message ์ด๋ฒคํŠธ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ฑ„ํŒ… ๋ฉ”์„ธ์ง€๋ฅผ ๋ฐ›์•˜์„ ๋•Œ ํ˜ธ์ถœ๋˜๋Š” ์ด๋ฒคํŠธ์ด๋‹ค. ๋‹จ ์ž๊ธฐ ์ž์‹ ์ด ๋ณด๋‚ธ ๋ฉ”์„ธ์ง€์—๋„ ์—ฌ๊ณผ์—†์ด ์ด๋ฒคํŠธ๊ฐ€ ํ˜ธ์ถœ๋˜๋ฏ€๋กœ response.sender.id === this.myUserId ์กฐ๊ฑด์„ ํ†ตํ•ด ์ž์‹ ์ด ๋ณด๋‚ธ ๋ฉ”์„ธ์ง€์—๋Š” ์•Œ๋ฆผ์„ ๋ณด์—ฌ์ฃผ์ง€ ์•Š๋„๋ก ์ฒ˜๋ฆฌํ•˜์˜€๋‹ค.

    ๊ทธ ๋‹ค์Œ์€ ์ด์ œ ์‚ฌ์šฉ์ž๊ฐ€ ์ž‘์€ ์•Œ๋ฆผ ๋ฉ”์„ธ์ง€๋งŒ ๋ณด๊ณ ๋„ ์–ด๋–ค ์ƒํ™ฉ์ด ๋ฒŒ์–ด์ง€๋Š” ๊ฒƒ์ธ์ง€ ์‰ฝ๊ฒŒ ์•Œ ์ˆ˜ ์žˆ๋„๋ก OOO๋‹˜์ด ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด๋ƒˆ์–ด์š”๋ผ๋Š” ํ˜•์‹์˜ ์ œ๋ชฉ๊ณผ ๋ฉ”์„ธ์ง€์˜ ๋‚ด์šฉ, ์ƒ๋Œ€๋ฐฉ์˜ ํ”„๋กœํ•„ ์‚ฌ์ง„์„ ์‚ฌ์šฉํ•˜์—ฌ Notification ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ๋์ด๋‹ค.

    push 필자의 멘탈 상태를 여과없이 보여주는 메세지 내용. 잉잉...

    ์–ด์จŒ๋“  ์ด๋ ‡๊ฒŒ ํ•ด์„œ ๋ธŒ๋ผ์šฐ์ €์— soomgo.com์ด ์—ด๋ ค์žˆ๋‹ค๋ฉด ์‚ฌ์šฉ์ž๋“ค์€ ๋‹ค๋ฅธ ์ผ์„ ํ•˜๋‹ค๊ฐ€ ๊ณ„์† ํŽ˜์ด์ง€๋ฅผ ํ™•์ธํ•˜๊ฑฐ๋‚˜ ํ•ธ๋“œํฐ์„ ํ™•์ธํ•  ํ•„์š”์—†์ด ๋ฐ์Šคํฌํƒ‘ ๋‚ด์—์„œ ์ƒˆ๋กœ์šด ์ฑ„ํŒ… ๋ฉ”์„ธ์ง€๋ฅผ ๋ฐ”๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๋‹ค.

    ์•„์ง๋„ Background ์ƒํƒœ์—์„œ ํ‘ธ์‹œ ๋ฉ”์„ธ์ง€๋ฅผ ๋ฐ”๋กœ ๋ณด์—ฌ์ฃผ์ง€ ๋ชปํ–ˆ๋‹ค๋Š” ๊ฒŒ ์•„์‰ฝ๊ธด ํ•˜๋‹ค. ํ•˜์ง€๋งŒ ์ด ์ •๋„๋งŒ ํ•ด๋„ ์‚ฌ์šฉ์ž๋“ค ์ž…์žฅ์—์„œ๋Š” ๊ฝค๋‚˜ ํŽธํ•  ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ•œ๋‹ค.

    ๋งˆ์น˜๋ฉฐ

    ์‚ฌ์‹ค ์ฒ˜์Œ ๋ชฉํ‘œํ–ˆ๋˜ ๊ฑธ ๋‹ค ์ด๋ฃจ์ง„ ๋ชปํ•ด์„œ ์ฐ์ฐํ–ˆ์ง€๋งŒ ๋„ˆ๋ฌด ํ”ผ๊ณคํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค์Œ์„ ๊ธฐ์•ฝํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค. ์ด์ œ ์›”์š”์ผ์— ์ถœ๊ทผํ•ด์„œ POํ•œํ…Œ ์ด๊ฑธ ๋ณด์—ฌ์ฃผ๊ณ  ํ˜น์‹œ ๋ญ ์ถ”๊ฐ€ํ•˜๊ณ  ์‹ถ์€ ๊ฑฐ ์—†๋Š”์ง€ ๋ฌผ์–ด๋ณด๊ณ  ๋ช‡๊ฐ€์ง€ ํ…Œ์ŠคํŠธ๋ฅผ ์ข€ ํ•ด๋ณธ ํ›„ ๋ฐฐํฌํ•  ์˜ˆ์ •์ด๋‹ค.

    ์„œ๋น„์Šค ์›Œ์ปค์— fetch ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด Add to Homescreen ๊ธฐ๋Šฅ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ์‚ฌ์‹ค ์ˆจ๊ณ ์˜ ํ”„๋ก ํŠธ์—”๋“œ ์ฑ•ํ„ฐ ๊ณต์‹ ์ž…์žฅ์€ ์‚ฌ์šฉ์ž๋“ค์ด ๋ชจ๋ฐ”์ผ ์›น๋ณด๋‹ค๋Š” ๋ชจ๋ฐ”์ผ ์•ฑ์„ ๋งŽ์ด ์‚ฌ์šฉํ–ˆ์œผ๋ฉด ํ•˜๋Š” ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ด๊ฑด ํ• ๊นŒ๋ง๊นŒ ๊ณ ๋ฏผ ์ค‘์ด๋‹ค.(์ธ์•ฑ ๋ธŒ๋ผ์šฐ์ € ํฌ๋กœ์Šค๋ธŒ๋ผ์šฐ์ง• ํ•˜๊ธฐ ์‹ซโ€ฆ)

    ์ผ๋‹จ ์ฒ˜์Œ ์Šค์ฝ”ํ”„๋ฅผ ๋„ˆ๋ฌด ํฌ๊ฒŒ ์žก์€ ๊ฒƒ ๊ฐ™๊ธฐ๋„ ํ•˜๋‹ค. ํ•˜๋‚˜ํ•˜๋‚˜ ์ข€ ์ž์„ธํžˆ ์•Œ์•„๋ณด๊ณ  ์ž‘์—…์„ ํ–ˆ์œผ๋ฉด ์ข‹์•˜์„ ๊ฒƒ ๊ฐ™์€๋ฐ ์›”์š”์ผ๋ถ€ํ„ฐ๋Š” ๋ฐ”๋กœ ๋˜ ํ•˜๋˜ ํ”„๋กœ์ ํŠธ ์ž‘์—…์„ ๋‹ค์‹œ ํ•ด์•ผํ•ด์„œ ๋งˆ์Œ์ด ๊ธ‰ํ–ˆ๋˜ ๊ฒƒ๋„ ์žˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ํšŒ์‚ฌ์— ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋ถ€์กฑํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ด๋Ÿฐ ๊ธฐ์ˆ ์ ์ธ ๊ธฐ๋Šฅ์„ ๋ถ™ํžˆ๋Š” ๊ฑด ์šฐ์„  ์ˆœ์œ„๊ฐ€ ๋‚ฎ์€ ํŽธ์ด๋ผ์„œ โ€œ์ง€๊ธˆ ์•„๋‹ˆ๋ฉด ์•ž์œผ๋กœ ์–ธ์ œ ํ•  ์ˆ˜ ์žˆ์„ ์ง€ ๋ชจ๋ฅธ๋‹คโ€๋ผ๋Š” ๋งˆ์Œ๋„ ์ปธ๋˜ ๊ฒƒ ๊ฐ™๋‹ค.

    ๋ง‰๊ฐ„์„ ์ด์šฉํ•ด, ํ•„์ž์™€ ํ•จ๊ป˜ ์ผํ•ด์ฃผ์‹ค ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž ๋ถ„์„ ๋ชจ์‹ ๋‹ค๋Š” JD๋ฅผ ๋ฟŒ๋ฆฌ๋ฉด์„œ ํฌ์ŠคํŒ…์„ ๋งˆ๋ฌด๋ฆฌ ํ•˜๊ฒ ๋‹ค. PWAย ์™ธ์—๋„ ๋‹ค๋ฅธ ํ•˜๊ณ  ์‹ถ์€ ๊ฑด ๋งŽ์€๋ฐ ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋ชจ์ž๋ผ์„œ ๋ชปํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ฒฝ๋ ฅ ์—ฌํ•˜์™€ ์ƒ๊ด€์—†์ด ๊ทธ๋ƒฅ ์žฌ๋ฐŒ๋Š” ๊ฑฐ ์ข‹์•„ํ•˜์‹œ๋Š” ๋ถ„์ด๋ฉด ๋œ๋‹ค.(ํ•˜๊ณ  ์‹ถ์€ ๊ฐœ๋ฐœ ๋‹ค ํ•˜์‹ค ์ˆ˜ ์žˆ๋„๋ก ์ด ํ•œ๋ชธ ๋ถˆ์‚ด๋ผ ๋ณดํ•„ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.)

    ์ด์ƒ์œผ๋กœ PWA ํ•˜๋ฃจ ๋งŒ์— ๋„์ž…ํ•˜๊ธฐ ํฌ์ŠคํŒ…์„ ๋งˆ์นœ๋‹ค.

    Evan Moon

    ๐Ÿข ๊ฑฐ๋ถ์ด์ฒ˜๋Ÿผ ์‚ด์ž

    ๊ฐœ๋ฐœ์„ ์ž˜ํ•˜๊ธฐ ์œ„ํ•ด์„œ๊ฐ€ ์•„๋‹Œ ๊ฐœ๋ฐœ์„ ์ฆ๊ธฐ๊ธฐ ์œ„ํ•ด ๋…ธ๋ ฅํ•˜๋Š” ๊ฐœ๋ฐœ์ž์ž…๋‹ˆ๋‹ค. ์‚ฌ์†Œํ•œ ์ƒ๊ฐ ์ •๋ฆฌ๋ถ€ํ„ฐ ํŠœํ† ๋ฆฌ์–ผ, ์‚ฝ์งˆ๊ธฐ ์ •๋„๋ฅผ ์ฃผ๋กœ ๋„์ ์ด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.