Skip to content

Heonys/topzl-desktop

Repository files navigation

Electron version

Main Page

๐Ÿš€ Introduction

Topzl์€ ๊ด‘๊ณ  ์—†๋Š” ๋ฌด๋ฃŒ ์Œ์•… ์ŠคํŠธ๋ฆฌ๋ฐ์„ ์œ„ํ•œ ๋ฐ์Šคํฌํƒ‘ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ž…๋‹ˆ๋‹ค. ์ตœ์‹  Electron ๋ฒ„์ „๊ณผ electron-vite๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ UI๋Š” React ํ™˜๊ฒฝ๊ณผ ํ†ตํ•ฉํ•˜์—ฌ ๊ฐœ๋ฐœ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํฌ๋กœ์Šค ํ”Œ๋žซํผ ์ง€์›์„ ํ†ตํ•œ ํ˜ธํ™˜์„ฑ๊ณผ ์•„๋ฆ„๋‹ค์šด ์ธํ„ฐํŽ˜์ด์Šค ๋ฐ ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค.

์†Œํ”„ํŠธ์›จ์–ด ๋‹ค์šด๋กœ๋“œ๋Š” ์ €์žฅ์†Œ์˜ Releases ํŽ˜์ด์ง€์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ๋Š” Windows ํ™˜๊ฒฝ์—์„œ๋งŒ ์•ˆ์ •์ ์œผ๋กœ ๋™์ž‘ํ•˜๊ธฐ์— Windows ์ „์šฉ์œผ๋กœ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค.

notice: ํ˜„์žฌ ๋ฆด๋ฆฌ์ฆˆ ๋ฒ„์ „์€ Windows ๋งŒ์„ ์ œ๊ณตํ•˜์ง€๋งŒ ํด๋ผ์ด์–ธํŠธ ํŒจํ‚ค์ง•์„ ํ†ตํ•ด์„œ ๋‹ค๋ฅธ ํ”Œ๋žซํผ์—์„œ ์ง์ ‘ ํŒจํ‚ค์ง•์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋งŒ ์ด ๊ฒฝ์šฐ ๋ฉ”๋‰ด, ํŠธ๋ ˆ์ด ๋“ฑ ์ผ๋ถ€ ๊ธฐ๋Šฅ์—์„œ ์ฐจ์ด๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โš ๏ธImportant

์ด ํ”„๋กœ์ ํŠธ๋Š” ็Œซๅคด็Œซ/MusicFreePlugins ์ €์žฅ์†Œ์˜ audiomack ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์‚ฌ์šฉํ•˜์—ฌ ์Œ์›์˜ ์žฌ์ƒ URL์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ๋‚ด๋ถ€์ ์œผ๋กœ๋Š” audiomack api๋ฅผ ์‚ฌ์šฉํ•˜์ง€๋งŒ ์œ ๋ฃŒ ์ปจํ…์ธ ๋Š” ํ•„ํ„ฐ๋ง๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ํ•™์Šต ๋ฐ ์ฐธ๊ณ  ์šฉ๋„๋กœ๋งŒ ์ œ๊ณต๋˜๋ฉฐ, ์ƒ์—…์  ์šฉ๋„๋กœ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„์•ผ ํ•˜๊ณ , ๋ฐ˜๋“œ์‹œ ํ•ฉ๋ฒ•์ ์œผ๋กœ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค๊ณ  ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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

โœจ Features

  • ํฌ๋กœ์Šค ํ”Œ๋žซํผ ์ง€์› (Windows, macOS, Linux)
  • ์Œ์•…, ์•จ๋ฒ”, ์•„ํ‹ฐ์ŠคํŠธ, ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ ๊ฒ€์ƒ‰
  • ์ž์ฒด ์ŠคํŠธ๋ฆฌ๋ฐ
  • ๋กœ์ปฌ ์Œ์•… ์žฌ์ƒ ์ง€์›
  • ์Œ์› ๋‹ค์šด๋กœ๋“œ ์ง€์›
  • ์›Œ์ปค ์Šค๋ ˆ๋“œ๋ฅผ ํ™œ์šฉํ•œ ๋กœ์ปฌ ํด๋” ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋™๊ธฐํ™”
  • ๊ฐ€์‚ฌ ์ง€์› (์›น ํฌ๋กค๋ง ๊ธฐ๋ฐ˜, ์ •ํ™•๋„ ๋ถˆ์•ˆ์ •)
  • ๋กœ๊ทธ์ธ ์—†์ด ์‚ฌ์šฉ ๊ฐ€๋Šฅ (์Šคํ† ๋ฆฌ์ง€ ๋ฐ AppData์— ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ์ €์žฅ)
  • ๋‹ค๊ตญ์–ด ์ง€์› (ํ•œ๊ตญ์–ด, ์˜์–ด)
  • ์‚ฌ์šฉ์ž ์ง€์ • ๋‹จ์ถ•ํ‚ค ์ง€์› (In-App, Global)
  • ์„ธ๋ถ€ ์„ค์ • ์ง€์› (์ผ๋ฐ˜, ์žฌ์ƒ, ๋‹ค์šด๋กœ๋“œ, ๊ฐ€์‚ฌ, ๋ฐฑ์—… ๋ฐ ๋ณต์›)
  • PIP ๋ชจ๋“œ ์ง€์›

๐Ÿ–ผ๏ธ Screenshot

์Šคํฌ๋ฆฐ์ƒท์„ ํ™•์ธ ํ•˜๋ ค๋ฉด ํŽผ์ณ์ฃผ์„ธ์š”

Main Search Search Album Detail Libray Palylist Local Download Pipmode Setting1

๐ŸŽ‰ Getting Started

  • ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์…‹์—…

# ์ €์žฅ์†Œ ํด๋ก 
git clone https://github.com/Heonys/topzl-desktop.git

# ์˜์กด์„ฑ ์„ค์น˜
yarn install

# ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰
yarn dev
  • ํด๋ผ์ด์–ธํŠธ ํŒจํ‚ค์ง•

ํ˜„์žฌ ๋ฆด๋ฆฌ์ฆˆ๋œ ๋ฒ„์ „์€ ์•ˆ์ •์ ์ธ Windows๋งŒ ์ œ๊ณต๋˜์ง€๋งŒ macOS์™€ Linux๋ฅผ ํด๋ผ์ด์–ธํŠธ์—์„œ ์ง์ ‘ ํŒจํ‚ค์ง• ํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋‹ค๋ฅธ ์šด์˜์ฒด์ œ ์—์„œ๋„ ์ง์ ‘ ํŒจํ‚ค์ง•ํ•˜์—ฌ ์‹คํ–‰์ด ๊ฐ€๋Šฅํ•˜๊ณ  electron-builder.json ํŒŒ์ผ ์—์„œ ๋นŒ๋“œ ์˜ต์…˜์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹จ, ๋ฆด๋ฆฌ์ฆˆ ๋ฒ„์ „๊ณผ ๋™์ผํ•˜๊ฒŒ ๋™์ž‘ํ•˜๋ฉด ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ GENIUS_ACCESS_TOKEN์„ ๋“ฑ๋ก ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Note: ์ž์„ธํ•œ ๋นŒ๋“œ ์„ค์ •์€ electron-builder ๋ฌธ์„œ ์—์„œ ํ™•์ธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

// electron-builder.json
"win": {
  "target": ["nsis", "zip"],
},
"mac": {
  "target": ["dmg"],
},
"linux": {
  "target": ["AppImage"],
},
# .env
MAIN_VITE_GENIUS_ACCESS_TOKEN="..."
yarn dist:{flatform} # [win, mac, linux]

๐Ÿงฉ Technical Detail

๐Ÿ”– ๋ชฉ๋ก

1. Electorn์˜ ๊ธฐ๋ณธ ๊ตฌ์กฐ ๋ฐ ๋™์ž‘์›๋ฆฌ

electron structure

Electron์€ ํฌ๋กœ์Šค ํ”Œ๋žซํผ ๋ฐ์Šคํฌํƒ‘ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ ์ž…๋‹ˆ๋‹ค. Chromium ์—”์ง„๊ณผ Node JS๋ฅผ ํ†ตํ•ฉํ•˜์—ฌ ์›น๊ธฐ์ˆ ์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋‹ค์–‘ํ•œ ํ”„๋ ˆ์ž„ ์›Œํฌ์™€ ํ†ตํ•ฉํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด ํŠน์ง•์ž…๋‹ˆ๋‹ค. ์ผ๋ ‰ํŠธ๋ก ์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค์™€ ๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค๋กœ ๋‚˜๋‰˜๊ฒŒ ๋‚˜๋‰˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

Main Process๋Š” ์œˆ๋„์šฐ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ์ผ๋ ‰ํŠธ๋ก  ์•ฑ ์ „์ฒด์˜ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ  ์ƒํ˜ธ์ž‘์šฉ ํ•ฉ๋‹ˆ๋‹ค. System API์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์–ด ๋ฐ์Šคํฌํƒ‘ ์•Œ๋ฆผ, ์‹œ์Šคํ…œ ํŠธ๋ ˆ์ด ๋“ฑ์˜ ๊ธฐ๋Šฅ ๋˜ํ•œ ์ƒํ˜ธ์ž‘์šฉ์ด ๊ฐ€๋Šฅํ•˜๋ฉฐ, Node.js ๋Ÿฐํƒ€์ž„์—์„œ ๋™์ž‘ํ•˜๊ธฐ์— npm ํŒจํ‚ค์ง€ ์‚ฌ์šฉ ๋˜๋Š” fs, os ๋“ฑ์˜ ๋‚ด์žฅ ๋ชจ๋“ˆ ๋˜ํ•œ ์—ญ์‹œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Renderer Process๋Š” ์‹ค์ œ ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋ Œ๋”๋งํ•˜๋Š” ํ”„๋กœ์„ธ์Šค๋กœ ๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค์—์„œ ๋งŒ๋“ค์–ด์ง„ ์œˆ๋„์šฐ๊ฐ€ ๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค์˜ ์‹คํ–‰ํ™˜๊ฒฝ์ด ๋˜์–ด UI๋ฅผ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ๋Š” Chromium ๊ธฐ๋ฐ˜์˜ ๋ธŒ๋ผ์šฐ์ € ์ฐฝ์„ ์ œ๊ณตํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค๋Š” ๊ฒฐ๊ตญ Node ํ™˜๊ฒฝ ์œ„์—์„œ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— ์ง์ ‘ ๋…ธ๋“œ ๋ชจ๋“ˆ์— ์ ‘๊ทผํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ผ๋ฐ˜์ ์ธ ์ผ๋ ‰ํŠธ๋ก  ๊ฐœ๋ฐœ์—์„  ๋ณด์•ˆ์„ ์œ„ํ•ด ๋ธŒ๋ผ์šฐ์ € ํ™˜๊ฒฝ๊ณผ ๋…ธ๋“œ ํ™˜๊ฒฝ์„ ์™„์ „ํžˆ ๊ฒฉ๋ฆฌ ์‹œ์ผœ์„œ ์‹คํ–‰ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ”„๋กœ์„ธ์Šค๊ฐ„์˜ IPC ํ†ต์‹ ์„ ํ†ตํ•ด์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›์œผ๋ฉฐ, ์•ˆ์ „ํ•œ IPCํ†ต์‹ ์„ ์œ„ํ•ด์„œ preload.js ํŒŒ์ผ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

// preload.js
const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("electron", {
  sendMessage: (msg) => ipcRenderer.send("message", msg),
});
// renderer process
window.electron.sendMessage("Hello from Renderer!");
// contextBridge์—์„œ api๋ฅผ ๋…ธ์ถœ์‹œ์ผฐ๊ธฐ ๋•Œ๋ฌธ์— window ๊ฐ์ฒด์— ์†์„ฑ์ด ์ถ”๊ฐ€๋จ

preload.js๋Š” ๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค๊ฐ€ ์ดˆ๊ธฐํ™” ๋˜๊ธฐ์ „์— ์‹คํ–‰๋˜๋Š” ์Šคํฌ๋ฆฝํŠธ๋กœ ๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค์™€ ์œ ์‚ฌํ•œ ๊ถŒํ•œ์„ ๊ฐ€์ง€๋Š” ํŠน์ˆ˜ํ•œ ํŒŒ์ผ์ž…๋‹ˆ๋‹ค. contextBridge๋ฅผ ํ†ตํ•ด์„œ ์•ˆ์ „ํ•˜๊ฒŒ ๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค์— ๋…ธ์ถœํ•˜๋ ค๋Š” API๋ฅผ ์ •์˜ํ•  ์ˆ˜ ์žˆ๊ณ , ์ด๋ฅผ ํ†ตํ•ด ๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค์™€ ๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค๊ฐ„์˜ ์•ˆ์ „ํ•œ ํ†ต์‹ ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๋Š” ์ค‘๊ฐ„๋‹ค๋ฆฌ ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.

์ฆ‰, ์ผ๋ ‰ํŠธ๋ก ์€ ๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž์™€ ์ƒํ˜ธ์ž‘์šฉ ํ•˜๋Š” UI๋ฅผ ์ œ๊ณตํ•˜๋ฉฐ, ๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค์™€์˜ IPC ํ†ต์‹ ์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•˜๊ณ  ํ™”๋ฉด์„ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

2. ๋‹ค์ค‘ ์œˆ๋„์šฐ๊ฐ„ ํ†ต์‹ 

๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค๋Š” ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์œˆ๋„์šฐ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ๊ฐ ๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค๋Š” ๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค์™€์˜ ํ†ต์‹ ์„ ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ IPCํ†ต์‹ ์œผ๋กœ๋Š” ๋ฉ”์ธ๊ณผ ๋ Œ๋”๋Ÿฌ๊ฐ„์˜ ์†Œํ†ต๋งŒ ๊ฐ€๋Šฅํ•˜๊ธฐ์— ๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค๊ฐ„ ์ƒํƒœ๋ฅผ ๊ณต์œ ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค์—์„œ ์ƒํƒœ๋ฅผ ์ค‘๊ฐœํ•ด์•ผ๋งŒ ํ•ฉ๋‹ˆ๋‹ค.

electron structure

ํ˜„์žฌ Topzl ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„  ๋ฉ”์ธ ์œˆ๋„์šฐ ์™ธ์— ์ž‘์€ ํ”Œ๋ ˆ์ด์–ด ํ˜•ํƒœ์˜ PIP๋ชจ๋“œ๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. PIP๋ชจ๋“œ๋Š” ๋ฉ”์ธ ์œˆ๋„์šฐ์˜ ์žฌ์ƒ์ƒํƒœ ๋˜๋Š” ํ˜„์žฌ ์žฌ์ƒ ์ค‘์ธ ๊ณก์˜ ์ •๋ณด๋ฅผ ๊ณต์œ  ํ•˜๋ฉฐ, ์žฌ์ƒ, ์ด์ „ ๊ณก, ๋‹ค์Œ ๊ณก์˜ ๋ฒ„ํŠผ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, PIP๋ชจ๋“œ๋ฅผ ์ œ๊ณตํ•˜๋Š” ์œˆ๋„์šฐ๋Š” ๋ฉ”์ธ ์œˆ๋„์šฐ์™€ ๋™์‹œ์— ์‹คํ–‰๋˜๋ฉฐ ์ƒํƒœ๋ฅผ ์ผ์ • ๋ถ€๋ถ„ ๊ณต์œ ํ•˜๊ณ  ๋ฐ˜๋Œ€๋กœ ๋ฉ”์ธ ์œˆ๋„์šฐ์˜ ์ƒํƒœ๋ฅผ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด์„œ IPCํ†ต์‹  ๋Œ€์‹  MessageChannelMain๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํฌํŠธ๊ฐ„์˜ ํ†ต์‹ ์„ ํ†ตํ•ด์„œ ๋ฉ”์‹œ์ง€ ์ „๋‹ฌ๊ณผ ์ƒํƒœ๋ฅผ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

// pipmode wiondow๊ฐ€ ์ƒ์„ฑ๋˜๋Š” ์‹œ์ ์— ํฌํŠธ๋ฅผ ์—ฐ๊ฒฐํ•˜์—ฌ mainwindow์˜ ์ƒํƒœ๋ฅผ ์ „๋‹ฌ
const { port1, port2 } = new MessageChannelMain();

mainWindow.webContents.postMessage("port", null, [port1]);
pipWindow.webContents.postMessage("port", { track: currentItem, state }, [port2]);

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๊ฐ ์œˆ๋„์šฐ๋“ค์€ ์ž์‹ ์ด ์—ฐ๊ฒฐ๋œ ํฌํŠธ๋กœ ๋ถ€ํ„ฐ ๋ฐ˜๋Œ€ํŽธ ํฌํŠธ์—๊ฒŒ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. mainWindow๋Š” port1๊ณผ ์—ฐ๊ฒฐ๋˜์–ด ํŠธ๋ž™์ด ๋ฐ”๋€Œ๊ฑฐ๋‚˜, ํ˜„์žฌ ํ”Œ๋ ˆ์ด์–ด์˜ ์ƒํƒœ๊ฐ€ ๋ฐ”๋€Œ๋ฉด port2์— ๋ฉ”์‹œ์ง€๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด pipmodeWinodw๋Š” port2์™€ ์—ฐ๊ฒฐ๋˜์–ด port1์œผ๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ์ƒํƒœ๋กœ ๋ถ€ํ„ฐ ํ˜„์žฌ ์œˆ๋„์šฐ์— ๋™๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค ๋˜ํ•œ pipmodeWindow์—์„œ๋„ ํŠธ๋ž™ ์ •๋ณด๋ฅผ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ์ง€๋งŒ, ์ง์ ‘ mainWindow์˜ ์ƒํƒœ๋ฅผ ๋ฐ”๊ฟ€ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— mainWindow์—์„  ๋ฏธ๋ฆฌ ๋ฉ”์‹œ์ง€ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์„ค์ •ํ•˜๊ณ  port1์—๊ฒŒ ํŠน์ • ์ด๋ฒคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋„๋ก ๋ฉ”์‹œ์ง€ ์ „๋‹ฌํ•˜๋„๋ก ํ•˜์—ฌ mainWindow์˜ ์ƒํƒœ๋ฅผ ๋ฐ”๊พธ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

3. ์›Œ์ปค ์Šค๋ ˆ๋“œ

Topzl ์—์„œ๋Š” ๋กœ์ปฌ ํŒŒ์ผ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์œ„ํ•œ FileWatcher ์›Œ์ปค์™€ ๋‹ค์šด๋กœ๋“œ๋ฅผ ์ง„ํ–‰ํ•˜๊ณ  ๊ทธ ๋‹ค์šด๋กœ๋“œ์˜ ์ƒํƒœ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ Œ๋”๋Ÿฌ์—๊ฒŒ ์ „๋‹ฌํ•ด ์ฃผ๋Š” Download ์›Œ์ปค ์ด๋ ‡๊ฒŒ 2๊ฐ€์ง€ ์›Œ์ปค ์Šค๋ ˆ๋“œ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. Comlink ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์™€ ์›Œ์ปค ์Šค๋ ˆ๋“œ๊ฐ„์˜ ์ƒํ˜ธ์ž‘์šฉ์„ ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋” ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์ž‘์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  • 1) ๋‹ค์šด๋กœ๋“œ ๋ฐ ๋‹ค์šด๋กœ๋“œ ์ƒํƒœ ๋™๊ธฐํ™”

monitoring

์Œ์› ๋‹ค์šด๋กœ๋“œ ๋˜ํ•œ ์›Œ์ปค ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ๋‹ค์šด๋กœ๋“œ๊ฐ€ ์‹œ์ž‘๋˜๋ฉด ๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค๋กœ ๋ถ€ํ„ฐ ์ „๋‹ฌ๋ฐ›์€ URL์„ ํŒจ์นญํ•ฉ๋‹ˆ๋‹ค. fetch API๋Š” ๋…ธ๋“œ์—์„œ๋„ ์ง€์›ํ•˜์ง€๋งŒ ์—ฌ๊ธฐ์„œ ์ฃผ์˜ํ•ด์•ผํ• ์ ์€ fetch๋กœ ํŒจ์นญ๋œ ๊ฒฐ๊ณผ๋Š” ReadableStream ๊ฐ์ฒด์ธ๋ฐ ์ด๋Š” ์›นํ™˜๊ฒฝ ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ์ŠคํŠธ๋ฆผ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฅผ Node์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ŠคํŠธ๋ฆผ์œผ๋กœ ๋ณ€ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ํ›„ writeStream์„ ๋งŒ๋“ค์–ด์„œ ํŒŒ์ดํ”„๋ผ์ธ์„ ์—ฐ๊ฒฐํ•ด ์ฃผ๊ณ , ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๊ฑฐ๋‚˜ ํŒŒ์ดํ•‘์ด ์™„๋ฃŒ๋˜๋ฉด ๋ Œ๋”๋Ÿฌ๋Ÿฌ ์—๊ฒŒ ์•Œ๋ ค ํ™”๋ฉด์„ ์—…๋ฐ์ดํŠธ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

async downloadFile(id: string, mediaSource: string, filePath: string) {
  const response = await fetch(mediaSource);
  const webStream = response.body as ReadableStream;
  const readableStream = Readable.fromWeb(webStream);
  const writeStream = fs.createWriteStream(filePath);

  pipeline(readableStream, writeStream, (err) => {
    if (err) {
      this.state = DownloadState.ERROR;
      this._onChange({ id, state: this.state, message: err.message });
      this.removeFile(filePath);
    } else {
      this.state = DownloadState.DONE;
      this._onChange({ id, state: this.state, current: total, total });
    }
  });
}
  • 2) ๋กœ์ปฌ ํด๋” ๋ชจ๋‹ˆํ„ฐ๋ง

monitoring

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

4. ํ”Œ๋Ÿฌ๊ทธ์ธ (Audiomack)

์Œ์•…์„ ์žฌ์ƒํ•˜๋ ค๋ฉด ์‹ค์ œ ์˜ค๋””์˜ค ํŒŒ์ผ์„ ์ œ๊ณตํ•˜๋Š” Media Source๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด ็Œซๅคด็Œซ/MusicFreePlugins ์ €์žฅ์†Œ์˜ audiomack ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. Audiomack์€ ๋ฌด๋ฃŒ๋กœ ์Œ์•…์„ ์ŠคํŠธ๋ฆฌ๋ฐํ•˜ ์„œ๋น„์Šค์ธ๋ฐ ์ด ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ๋‚ด๋ถ€์ ์œผ๋กœ๋Š” Audiomack API์„ ์‚ฌ์šฉํ•˜๋ฉฐ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๊ฒ€์ƒ‰ ๋ฐ ๊ณก์˜ ์žฌ์ƒ URL์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š” ๋ฉ”์†Œ๋“œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

// ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ํƒ€์ž…, ๊ธฐ๋ณธ์ ์œผ๋กœ ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ง€์›
type SearchResult = {
  isEnd: boolean;
  data: {
    id: string;
    album: string;
    artist: string;
    artwork: string;
    duration: number;
    title: string;
  }
};

์ด๋ ‡๊ฒŒ ๊ฒ€์ƒ‰๋œ ์Œ์›์˜ ID๋ฅผ ์ด์šฉํ•ด Media Source URL์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ๋Š” ๋งŒ๋ฃŒ ์‹œ๊ฐ„, ์‹œ๊ทธ๋‹ˆ์ณ, ํ‚ค ํŽ˜์–ด๊ฐ€ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํฌํ•จ๋˜์–ด ์žˆ๊ณ  ์•„๋ž˜์™€ ๊ฐ™์€ ํ˜•ํƒœ์˜ URL์ž…๋‹ˆ๋‹ค.

https://music.audiomack.com/albums/r0m1/red-planet/5ad60e011e7e3.mp3?${Parameters}

5. ๊ฐ€์‚ฌ ๊ฒ€์ƒ‰

๊ธฐ๋ณธ์ ์œผ๋กœ Genius API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์Œ์›์˜ ๊ฐ€์‚ฌ๋ฅผ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.

Genius๋Š” ๋ฏธ๊ตญ์—์„œ ์Œ์•… ๊ฐ€์‚ฌ๋ฅผ ์ œ๊ณตํ•˜๋Š” ์„œ๋น„์Šค๋กœ ๋น„์˜์–ด๊ถŒ์˜ ์Œ์•…์˜ ๊ฒฝ์šฐ ์˜์–ด ๋ฐœ์Œ๋Œ€๋กœ ํ‘œ๊ธฐํ•˜๋Š” ๋กœ๋งˆ์ž ํ‘œ๊ธฐ๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฐ ๊ฒฝ์šฐ, ๋ณดํ†ต ์›๊ณก ์–ธ์–ด๋กœ ๋œ ๊ฐ€์‚ฌ๋„ ์ œ๊ณตํ•˜์ง€๋งŒ ๊ธฐ๋ณธ ์–ธ์–ด๋Š” ๋กœ๋งˆ์ž ํ‘œ๊ธฐ ๋˜์–ด์žˆ๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ํ•œ๊ตญ์–ด ๊ณก์ด๋ผ๋„ ์›๊ณก ๊ฐ€์‚ฌ๊ฐ€ ์•„๋‹Œ ์˜์–ด ๋ฐœ์Œ๋Œ€๋กœ ๋ณ€ํ™˜๋œ ๋กœ๋งˆ์ž ํ‘œ๊ธฐ๊ฐ€ ์ œ๊ณต๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Genius์—์„  ๋น„์˜์–ด๊ถŒ์˜ ๋…ธ๋ž˜์˜ ๊ฒฝ์šฐ ์›๊ณก ์–ธ์–ด์˜ ๊ฐ€์‚ฌ๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์ง€๋งŒ, ๊ธฐ๋ณธ์–ธ์–ด๋Š” ๋กœ๋งˆ์ž๋กœ ๋˜์–ด์žˆ๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ Genius API์—์„œ๋Š” ๋ฒˆ์—ญ๋œ ๊ฐ€์‚ฌ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜์ง€ ์•Š์•„์„œ ๊ธฐ๋ณธ์–ธ์–ด๋งŒ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ Genius API๋งŒ์„ ์‚ฌ์šฉํ• ๊ฒฝ์šฐ ๊ธฐ๋ณธ์–ธ์–ด ์ด์™ธ์— ๋ฒˆ์—ญ๋œ ๊ฐ€์‚ฌ๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ์–ด๋ ค์šด ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ Genius API ๊ธฐ๋ฐ˜์ด๋ฉด์„œ ์›น ํฌ๋กค๋ง ๊ธฐ๋Šฅ์„ ์ง€์›ํ•˜๋Š” genius-lyrics ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค. Topzl์˜ ์„ค์ • ํŽ˜์ด์ง€์—์„  ๊ฐ€์‚ฌ ๊ฒ€์ƒ‰ ์‹œ ๊ฒ€์ƒ‰ ๋ฐฉ์‹์œผ๋กœ ๊ธฐ๋ณธ๊ฒ€์ƒ‰๊ณผ ์ •๋ฐ€๊ฒ€์ƒ‰์„ ์ œ๊ณตํ•˜๋Š”๋ฐ ๊ธฐ๋ณธ๊ฒ€์ƒ‰์€ Genius API์˜ ๊ฒ€์ƒ‰์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•ด์„œ ๊ฒ€์ƒ‰ํ•˜์—ฌ ๋น ๋ฅธ์†๋„๋ฅผ ์ œ๊ณตํ•˜์ง€๋งŒ ๋น„์˜์–ด๊ถŒ์˜ ์Œ์•…์˜ ๊ฒฝ์šฐ ๋กœ๋งˆ์ž ํ‘œ๊ธฐ์˜ ๊ฐ€์‚ฌ์ผ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด, ์ •๋ฐ€ ๊ฒ€์ƒ‰์˜ ๊ฒฝ์šฐ๋Š” genius-lyrics์˜ ์›น ํฌ๋กค๋ง์„ ํ†ตํ•ด์„œ ๋ฒˆ์—ญ๋œ ๊ฐ€์‚ฌ๋ฅผ ํ™•์ธํ•˜๊ณ  ๋กœ๋งˆ์ž ํ‘œ๊ธฐ๊ฐ€ ์•„๋‹Œ ์›๊ณก ์–ธ์–ด์˜ ๊ฐ€๋Šฅ์„ฑ์„ ๋†’์ž…๋‹ˆ๋‹ค.

const searchMethod = await getAppConfigPath("lyric.searchMethod"); // ๊ฒ€์ƒ‰๋ฐฉ์‹์„ ๊ฐ€์ ธ์˜ค๊ธฐ
const songs = await client.songs.search(query); // ๊ฒ€์ƒ‰์–ด๋กœ ๊ฐ€์‚ฌ ๊ฒ€์ƒ‰

if (searchMethod === "basic") {
  return songs[0].lyrics(false); // ๊ธฐ๋ณธ ๊ฒ€์ƒ‰: ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์˜ ์ฒซ๋ฒˆ์งธ ๊ฐ€์‚ฌ ๋ฐ˜ํ™˜
} else {
  const scrapedData = await client.songs.scrape(songs[0].url);
  const scrapedSong = Object.values(scrapedData.data.entities.songs)[0]
  // ์ •๋ฐ€ ๊ฒ€์ƒ‰: ์ฒซ๋ฒˆ์งธ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํฌ๋กค๋ง ํ›„, ๋ฒˆ์—ญ๋œ ๊ฐ€์‚ฌ๋ฅผ ์ฐพ๊ณ  ๋ฐ˜ํ™˜
}

ํ•ด๋‹น ์Œ์•…์ด ๋ฒˆ์—ญ๋œ ๊ฐ€์‚ฌ๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š์„ ์ˆ˜๋„ ์žˆ์„๋ฟ๋”๋Ÿฌ ํšจ์œจ์„ฑ์„ ์œ„ํ•ด์„œ ์ •ํ™•๋„๊ฐ€ ๊ฐ€์žฅ ๋†’์„ ์ˆ˜ ์žˆ๋Š” ์ฒซ ๋ฒˆ์งธ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋งŒ ํ™•์ธ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ€๋Šฅํ•œ ๋กœ๋งˆ์ž ํ‘œ๊ธฐ๋ฅผ ํ”ผํ•˜๋Š” ์‹์œผ๋กœ ๊ฒ€์ƒ‰์„ ํ•˜์ง€๋งŒ ์ •ํ™•๋„๊ฐ€ ๋–จ์–ด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

6. ๊ฐ€์ƒ ์Šคํฌ๋กค (useVirtualScroll)

type Props<T> = {
  getScrollElement: () => HTMLElement;
  estimizeItemHeight: number;
  data: T[];
  renderCount?: number;
}

type VirtualItem<T> = {
  top: number;
  rowIndex: number;
  dataItem: T;
}

์žฌ์ƒ๋ชฉ๋ก์ด ๋งŽ์•„์งˆ ๊ฒฝ์šฐ, ์žฌ์ƒ๋ชฉ๋ก์˜ ํšจ์œจ์ ์ธ ๋ Œ๋”๋ง์„ ์œ„ํ•ด์„œ ๊ฐ€์ƒ ์Šคํฌ๋กค์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ useVirtualScroll๋ผ๋Š” ํ›…์„ ์‚ฌ์šฉํ•˜์—ฌ ์›๋ณธ ์žฌ์ƒ๋ชฉ๋ก์„ VirtualItemํƒ€์ž… ๋ฐฐ์—ด๋กœ ๋ฐ˜ํ™˜ํ•˜์—ฌ ํ™”๋ฉด์— ๋ Œ๋”๋งํ•  ํ•ญ๋ชฉ์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. Props๋กœ๋Š” ์Šคํฌ๋กค ์ด๋ฒคํŠธ๋ฅผ ์ถ”์ ํ•˜๊ธฐ ์œ„ํ•œ ์Šคํฌ๋กคํ•  DOM์š”์†Œ๋ฅผ ref๋กœ ์ „๋‹ฌ๋ฐ›๊ณ , ๊ฐ ๋ฆฌ์ŠคํŠธ์˜ ์˜ˆ์ƒ ๋†’์ด, ๋ฐ์ดํ„ฐ ๋ฐฐ์—ด, ์ œํ•œํ•˜๊ธฐ ์œ„ํ•œ ๊ฐœ์ˆ˜๋ฅผ ์ „๋‹ฌ๋ฐ›์Šต๋‹ˆ๋‹ค.

const virtualController = useVirtualScroll(props)
return (
  <div ref={scrollElementRef}>
    <div
      style={{
        position: "relative"
        height: virtualController.totalHeight
      }}
    >
      {virtualController.virtualItems.map(({rowIndex, top}) => {
        return (
        <div key={rowIndex} style={{ position: "absolute", top }}>
          {/* ๊ฐ ํ•ญ๋ชฉ ๋‚ด์šฉ */}
        </div>
        );
      })}
    </div>
  </div>
)

useVirtualScroll์œผ๋กœ ๋ฐ˜ํ™˜๋œ VirtualItem[] ํƒ€์ž…์˜ ๋ฐฐ์—ด์„ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ, ๊ฐ ํ•ญ๋ชฉ์€ top์œ„์น˜๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋ Œ๋”๋ง ๋ฉ๋‹ˆ๋‹ค. ์ „์ฒด ๋†’์ด๋ฅผ ์„ค์ •ํ•˜๊ณ  ๋ Œ๋”๋ง์„ ์ œํ•œํ•œ ๊ฐœ์ˆ˜๋งŒํผ ํ•ญ๋ชฉ๋“ค์„ ํ™”๋ฉด์— ๋ Œ๋”๋งํ•˜์—ฌ, ๋งŽ์€ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์„๊ฒฝ์šฐ ํ™”๋ฉด์— ํ•œ ๋ฒˆ์— ๋ Œ๋”๋ง๋˜๋Š” ํ•ญ๋ชฉ์„ ์ œํ•œํ•จ์œผ๋กœ์จ ํšจ์œจ์ ์œผ๋กœ ๋ Œ๋”๋ง ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

7. ์žฌ์ƒ๋ชฉ๋ก ์ •๋ ฌ (Drag & Drop)

monitoring

์žฌ์ƒ๋ชฉ๋ก ๋ฐ ์žฌ์ƒ๋ชฉ๋ก ํ…Œ์ด๋ธ”์—์„œ ๊ฐ ํ•ญ๋ชฉ์„ ๋“œ๋ž˜๊ทธ ๋“œ๋ž์œผ๋กœ ์œ„์น˜๋ฅผ ๋ฐ”๊พธ๋Š” ๊ธฐ๋Šฅ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

๊ตฌํ˜„ ์›๋ฆฌ

  1. position: absolute ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ ํ•ญ๋ชฉ์˜ ์œ„ ๋˜๋Š” ์•„๋ž˜์— ๋“œ๋กญ ๊ฐ€๋Šฅํ•œ ์ž‘์€ Droppable ์˜์—ญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  2. ์‚ฌ์šฉ์ž๊ฐ€ ํ•ญ๋ชฉ์„ ๋“œ๋ž˜๊ทธ๋ฅผ ์‹œ์ž‘ํ•˜๋ฉด dataTransfer๋ฅผ ํ™œ์šฉํ•ด ๋“œ๋ž˜๊ทธ๊ฐ€ ์‹œ์ž‘๋œ ํ•ญ๋ชฉ์˜ index๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
  3. Droppable ์˜์—ญ์— onDrop ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด, ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ index์™€ ๋“œ๋กญ๋œ ์œ„์น˜์˜ index๋ฅผ ๋น„๊ตํ•˜์—ฌ ๋ฐฐ์—ด์—์„œ ์ˆœ์„œ๋ฅผ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.
const Droppable = (props) => {
  const [isDragOver, setIsDragOver] = useState(false);

  return (
      <div
        style={{ position: "absolute" }}
        onDragOver={(e)=>{
          e.preventDefault();
          setIsDragOver(true); // ๋“œ๋ž˜๊ทธ ๊ฐ€๋Šฅํ•œ ์˜์—ญ์— ๋“ค์–ด์™”์Œ์„ ํ‘œ์‹œ
        }}
        onDragLeave={()=> setIsDragOver(false)} // ์˜์—ญ์„ ๋ฒ—์–ด๋‚จ
        onDrop={(e) => {
          e.preventDefault();
          setIsDragOver(false);
          // ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ index์™€ ๋“œ๋กญ๋œ index๋ฅผ ๋น„๊ตํ•˜์—ฌ ์ˆœ์„œ ๋ณ€๊ฒฝ
        }}
      >
        {isDragOver && <div> {/* ๋“œ๋ž˜๊ทธ ๊ฐ€๋Šฅํ•œ ์˜์—ญ UI */} </div>}
      </div>
  )
}

ํ˜„์žฌ ์žฌ์ƒ๋ชฉ๋ก์€ useVirtualScroll๋กœ ๋ž˜ํ•‘๋œ ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๊ธฐ์— ๋ฐฐ์—ด์˜ index๊ฐ€์•„๋‹Œ VirtualItem์˜ rowIndex๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ๊ฐ€์ƒ ์Šคํฌ๋กค์„ ์‚ฌ์šฉํ•˜๋ฉด์„œ ์ •์ƒ์ ์œผ๋กœ ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋ž๊ธฐ๋Šฅ์ด ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

8. Scroll Navigator

monitoring

์‚ฌ์šฉ์ž๊ฐ€ ์Šคํฌ๋กคํ•  ๋•Œ ํ˜„์žฌ ๋ณด๊ณ  ์žˆ๋Š” ์„น์…˜์„ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•˜์—ฌ ๋„ค๋น„๊ฒŒ์ด์…˜ UI๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ Scrollspy๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ณผ์ •์—์„œ IntersectionObserver๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ตฌํ˜„ ์›๋ฆฌ

  1. ํ˜„์žฌ ์„ ํƒ๋œ ์„น์…˜์„ ๊ด€๋ฆฌํ•˜๋Š” state๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  2. ๊ฐ ์„น์…˜์„ ๋งˆํฌ์—…ํ•  ๋•Œ ๊ณ ์œ ํ•œ id๋ฅผ ๋ถ€์—ฌํ•ฉ๋‹ˆ๋‹ค.
  3. IntersectionObserver๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ ์„น์…˜์ด ๋ทฐํฌํŠธ์™€ ๊ต์ฐจ๋˜๋Š” ๋น„์œจ์„ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค.
  4. ๊ฐ€์žฅ ๋งŽ์ด ๊ต์ฐจ๋œ ์„น์…˜์„ state๋กœ ์—…๋ฐ์ดํŠธํ•˜์—ฌ Scrollspy๋ฅผ ๋™์ ์œผ๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.
const [selected, setSelected] = useState(routers[0].id);
const intersectionObserverRef = useRef<IntersectionObserver>();
const intersectionRatioRef = useRef<Map<string, number>>(new Map());

useEffect(() => {
  const ratioMap = intersectionRatioRef.current;
  intersectionObserverRef.current = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        ratioMap.set(entry.target.id, entry.intersectionRatio);
      });
      // ratioMap์—์„œ ๊ฐ€์žฅ ๋†’์€ ๋น„์œจ์˜ ์„น์…˜์œผ๋กœ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝ
    },
    options
  );
  // ์˜ต์ €๋ฒ„ ๋“ฑ๋ก (๊ฐ ์„น์…˜ ๊ฐ์‹œ ์‹œ์ž‘)
}, []);

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์Šคํฌ๋กค ์‹œ ๊ฐ€์žฅ ๋งŽ์ด ๊ต์ฐจ๋œ ์„น์…˜์ด ์ž๋™์œผ๋กœ ์„ ํƒ๋˜๋ฉฐ, ์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์Šคํฌ๋กค ์œ„์น˜์— ๋”ฐ๋ผ์„œ Scrollspy๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

9. Focus์™€ Blur ์ด๋ฒคํŠธ ํ๋ฆ„ ์ œ์–ด

monitoring

์ƒ๋‹จ ๋ฉ”๋‰ด๋Š” ํ€ต์„œ์น˜๋ฅผ ์œ„ํ•œ input ํผ์„ ์ œ๊ณตํ•˜๋ฉฐ focus์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด history๊ฐ€ ํ‘œ์‹œ๋˜๊ณ  blur ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด history๋ฅผ ๋‹ซ๋Š” ๋™์ž‘์„ ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ history๊ฐ€ ์—ด๋ฆฐ์ƒํƒœ์—์„œ ๋‚ด๋ถ€๋ฅผ ํด๋ฆญํ•˜๋ฉด ๋‚ด๋ถ€์˜ ํฌ์ปค์‹ฑ๋ณด๋‹ค input์˜ blur ์ด๋ฒคํŠธ๊ฐ€ ๋จผ์ € ์ผ์–ด๋‚˜ ์ฐฝ์ด ๋ฐ”๋กœ ๋‹ซํžˆ๊ฒŒ ๋˜์–ด, ๋‚ด๋ถ€์˜ ๋ฒ„ํŠผ์ด ๋™์ž‘ํ•˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

const isFocusedRef = useRef(false);

return (
  <input
    onFocus={() => openHistory()}
    onBlur={() => {
      setTimeout(() => {
        if (!isFocusedRef.current) closeHistory();
      });
    }}
  />
  <SearchHistory
    onFocus={() => {
      isFocusedRef.current = true;
    }}
    onBlur={() => {
      isFocusedRef.current = false;
      closeHistory();
    }}
  />
)

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด, ref๋ฅผ ์ƒ์„ฑํ•˜์—ฌ history๊ฐ€ ์—ด๋ฆฐ ์ƒํƒœ์—์„œ ์ž…๋ ฅ ํ•„๋“œ์˜ ํฌ์ปค์Šค ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  input์˜ blur ์ด๋ฒคํŠธ์—์„œ๋Š” setTimeout์„ ์‚ฌ์šฉํ•ด history๋ฅผ ๋‹ซ๋Š” ๋™์ž‘์„ ๋‹ค์Œ ํ”„๋ ˆ์ž„์œผ๋กœ ๋ณด๋‚ด์–ด blur ์ด๋ฒคํŠธ๊ฐ€ focus๋ณด๋‹ค ๋” ๋Šฆ๊ฒŒ ๋ฐœ์ƒํ•˜๋„๋ก ํ•˜์—ฌ focus ์ด๋ฒคํŠธ๊ฐ€ ์šฐ์„  ์ฒ˜๋ฆฌ๋˜๋Š” ๊ฒƒ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ณผ์ •์—์„œ searchHistory๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ focus์ด๋ฒคํŠธ๊ฐ€ ์ผ์–ด๋‚˜์ง€ ์•Š๊ธฐ์— tabindex ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํฌ์ปค์‹ฑ์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

10. ์ปจํ…์ŠคํŠธ ๋ฉ”๋‰ด

์žฌ์ƒ๋ชฉ๋ก์—์„œ ๋งˆ์šฐ์Šค ์šฐํด๋ฆญ ์‹œ ์žฌ์ƒ๋ชฉ๋ก์˜ ์•จ๋ฒ” ์ปค๋ฒ„์™€ ํ•จ๊ป˜ ์ถ”๊ฐ€ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ์ปจํ…์ŠคํŠธ ๋ฉ”๋‰ด๊ฐ€ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค. ์ด๋•Œ ์ปจํ…์ŠคํŠธ ๋ฉ”๋‰ด๋Š” ํ˜„์žฌ ๋งˆ์šฐ์Šค์˜ ์œ„์น˜์— ๋”ฐ๋ผ์„œ ๋ฉ”๋‰ด๋ฅผ ์–ด๋А ๋ฐฉํ–ฅ์œผ๋กœ ๋ณด์—ฌ์ค˜์•ผ ํ• ์ง€ ์„ ํƒ๋ผ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋งˆ์šฐ์Šค๊ฐ€ ์šฐ์ธก ์ƒ๋‹จ์—์„œ ์ปจํ…์ŠคํŠธ ๋ฉ”๋‰ด๊ฐ€ ์—ด๋ฆฐ๋‹ค๋ฉด ๋ฉ”๋‰ด๊ฐ€ ํ™”๋ฉด์—์„œ ๊ฐ€๋ ค์ง„๋‹ค๊ฑฐ๋‚˜ ์Šคํฌ๋กค์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

// OFFSET: ๋งˆ์šฐ์Šค์™€ ๋ฉ”๋‰ด๊ฐ€ ๋„ˆ๋ฌด ๋”ฑ ๋ถ™์–ด์žˆ์ง€ ์•Š๊ธฐ ์œ„ํ•œ ๊ฐ„๊ฒฉ
function computedPosition(x: number, y: number, count: number, padding: number) {
  const MAX_HEIGHT = count * MENU_ITEM_HEIGHT + padding;
  const isLeft = x < window.innerWidth / 2 ? 0 : 1;
  const isTop = y < window.innerHeight / 2 ? 0 : 2;
  switch (isLeft + isTop) {
    case 0: // 2์‚ฌ๋ถ„๋ฉด
      return [x + OFFSET, y + OFFSET];
    case 1: // 1์‚ฌ๋ถ„๋ฉด
      return [x - MENU_ITEM_WIDTH - OFFSET, y + OFFSET];
    case 2: // 3์‚ฌ๋ถ„๋ฉด
      return [x + OFFSET, y - MAX_HEIGHT - OFFSET];
    case 3: // 4์‚ฌ๋ถ„๋ฉด
      return [x - MENU_ITEM_WIDTH - OFFSET, y - MAX_HEIGHT - OFFSET];
  }
}

์žฌ์ƒ๋ชฉ๋ก์—์„œ onContextMenu์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ๋งˆ์šฐ์Šค์˜ ์ขŒํ‘œ๋ฅผ ๊ณ„์‚ฐํ•˜์—ฌ ํ˜„์žฌ ๋ทฐํฌํŠธ์—์„œ์˜ ์œ„์น˜๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ์œผ๋กœ ๋ฉ”๋‰ด๊ฐ€ ์—ด๋ฆด ๋ฐฉํ–ฅ์„ ๊ฒฐ์ •ํ•˜๊ณ  ์ด๋ฅผ ํ†ตํ•ด ํ•ญ์ƒ ํ™”๋ฉด ๋‚ด์—์„œ ๋ฉ”๋‰ด๊ฐ€ ํ‘œ์‹œ๋˜๋„๋ก ๋ณด์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

11. EventEmitter

์Œ์•… ์žฌ์ƒ๊ณผ ๊ด€๋ จ๋œ ์ด๋ฒคํŠธ ๋ฐ ๋‹จ์ถ•ํ‚ค ์ž…๋ ฅ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด eventemitter3 ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” node:events ๋ชจ๋“ˆ์˜ EventEmitter์™€ ์œ ์‚ฌํ•˜์ง€๋งŒ ๋ธŒ๋ผ์šฐ์ €์—์„œ๋„ ์‚ฌ์šฉ๊ฐ€๋Šฅํ•˜๋ฉฐ DOM ์ด๋ฒคํŠธ์™€๋Š” ๋ณ„๊ฐœ๋กœ ๋…๋ฆฝ์ ์ธ ์ด๋ฒคํŠธ ์‹œ์Šคํ…œ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

// setupPlayer
import EventEmitter from "eventemitter3";

const playerEventEmitter = new EventEmitter()

playerEventEmitter.on("play-end", () => {
  // ๊ณก์ด ๋๋‚ฌ์„๋•Œ ๋ฐœ์ƒํ•˜๋ฏ€๋กœ ๋ฐ˜๋ณต ๋ชจ๋“œ์— ๋”ฐ๋ผ์„œ ๋‹ค์Œ ๋™์ž‘์„ ์ฒ˜๋ฆฌ
});
// TrackPlayer
this.$audio.onended = () => {
  playerEventEmitter.emit("play-end");
};

์œ„์˜ ์ฝ”๋“œ๋Š” TrackPlayer์—์„œ ๊ณก์ด ์ข…๋ฃŒ๋  ๋•Œ play-end ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ณ , ์ด๋ฅผ ํ•ธ๋“ค๋Ÿฌ์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ์•ฑ์ด ์‹œ์ž‘๋  ๋•Œ ๋ฏธ๋ฆฌ ๊ฐ ์ด๋ฒคํŠธ์— ๋Œ€ํ•œ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๋งŒ๋“ค์–ด๋‘๊ณ  ์ดํ›„์— ํ”Œ๋ ˆ์ด์–ด์—์„œ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์ง์ ‘ ์ฒ˜๋ฆฌํ•˜๋Š”๊ฒŒ ์•„๋‹Œ ๋…๋ฆฝ์ ์ธ ์ด๋ฒคํŠธ ์‹œ์Šคํ…œ์„ ํ™œ์šฉํ•˜์—ฌ ์ด๋ฒคํŠธ๋“ค์„ ํ•œ๊ณณ์—์„œ ๊ด€๋ฆฌํ•˜๋ฉฐ ์ฝ”๋“œ์˜ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

12. ๋‹จ์ถ•ํ‚ค ๋“ฑ๋ก (In-App, Global)

์„ค์ • ํŽ˜์ด์ง€์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ๋‹จ์ถ•ํ‚ค๋ฅผ ์ปค์Šคํ…€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

monitoring

  • In-App ๋‹จ์ถ•ํ‚ค: ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ํฌ์ปค์Šค๋œ ์ƒํƒœ์—์„œ๋งŒ ๋™์ž‘
  • Global ๋‹จ์ถ•ํ‚ค: ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋‹ค๋ฅธ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‚ฌ์šฉ ์ค‘์—๋„ ๋™์ž‘

In-App๋‹จ์ถ•ํ‚ค๋Š” hotkeys-js๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค์—์„œ ๊ด€๋ฆฌํ•˜๋ฉฐ, Global ๋‹จ์ถ•ํ‚ค๋Š” ๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค์—์„œ electron ๋ชจ๋“ˆ์˜ globalShortcut API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹œ์Šคํ…œ์— ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. ๋‚ด๋ถ€์ ์œผ๋กœ๋Š” EventEmitter์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ ๊ธฐ๋Šฅ๋“ค์— ๋Œ€ํ•œ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๋“ฑ๋กํ•ด ๋†“๊ณ  ์ดํ›„ ์‚ฌ์šฉ์ž๊ฐ€ ํŠน์ • ๋‹จ์ถ•ํ‚ค๋ฅผ ์„ค์ •ํ•˜๋ฉด, keydown ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ ํ•ด๋‹น ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์‹คํ–‰๋˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

13. ๋กœ์ปฌ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ

ํ˜„์žฌ ๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค์™€ ๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค์—์„œ ๋กœ์ปฌ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด์„œ ์„ธ ๊ฐ€์ง€ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

  • JSON ํŒŒ์ผ (๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค)
  • ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ (๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค)
  • IndexedDB (๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค)

๋ชจ๋“  ์‚ฌ์šฉ์ž ์„ค์ •์€ Windows ํ™˜๊ฒฝ ๊ธฐ์ค€์œผ๋กœ AppData\Roaming\topzl\config.json ํŒŒ์ผ์—์„œ ๊ด€๋ฆฌ๋˜์–ด ๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค์™€ ๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค์—์„œ ์„ค์ • ๋ฐ์ดํ„ฐ๋ฅผ ๊ณต์œ ๋ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์žฌ์ƒ ์ค‘์ธ ๊ณก, ๋ณผ๋ฅจ, ์žฌ์ƒ์†๋„, ๋ฐ˜๋ณต๋ชจ๋“œ, ์…”ํ”Œ๋ชจ๋“œ ๋“ฑ์˜ ๋น„๊ต์  ํœ˜๋ฐœ์„ฑ ๋ฐ์ดํ„ฐ์— ๊ฐ€๊น๊ณ  ์ข€ ๋” ๋‹จ์ˆœํ•œ ํƒ€์ž…์˜ ๋ฐ์ดํ„ฐ๋Š” ์Šคํ† ๋ฆฌ์ง€๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด ํ˜„์žฌ ์žฌ์ƒ๋ชฉ๋ก, ์ „์ฒด ์žฌ์ƒ๋ชฉ๋ก ๋“ฑ์˜ ์ปฌ๋ ‰์…˜ ๋ฐ์ดํ„ฐ ์ฒ˜๋Ÿผ ๋Œ€์šฉ๋Ÿ‰์ด ๋  ์ˆ˜ ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋Š” IndexedDB๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

14. ํ™”๋ฉด ์บก์ฒ˜

electron ๋ชจ๋“ˆ์˜ desktopCapturer API๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ ์ „์ฒด ํ™”๋ฉด ๋˜๋Š” ํŠน์ • ์œˆ๋„์šฐ์ฐฝ์˜ ๊ณ ์œ ํ•œ ์‹๋ณ„์ž id๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์‹๋ณ„์ž๋ฅผ ์ด์šฉํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ €์—์„œ ํ•ด๋‹น ํ™”๋ฉด ๋˜๋Š” ์œˆ๋„์šฐ์ฐฝ์„ ์ŠคํŠธ๋ฆฌ๋ฐ ํ•  ์ˆ˜ ์žˆ๋Š” MediaStream๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๋Š”๋ฐ ์ด ์ŠคํŠธ๋ฆผ ๋ฐ์ดํ„ฐ๋ฅผ <video> ํƒœ๊ทธ์—์„œ ์žฌ์ƒํ•  ์ˆ˜ ์žˆ๊ณ , ํ•ด๋‹น ์ŠคํŠธ๋ฆผ์˜ ์ฒซ๋ฒˆ์งธ ํ”„๋ ˆ์ž„์„ <canvas> ํƒœ๊ทธ์—์„œ ๊ทธ๋ฆฌ๋ฉด ํ™”๋ฉด ์บก์ฒ˜๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

const handleDesktopCapture = async () => {
  const sourceId = await window.common.getDesktopCaptureId();
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: false,
    video: {
      mandatory: { chromeMediaSource: "desktop", chromeMediaSourceId: sourceId },
    }
  });

  $video.srcObject = stream;
  $video.onloadedmetadata = () => {
    $video.play();
    drawCanvas();
  };
};

const drawCanvas = () => {
  const ctx = $canvas.getContext("2d");
  ctx?.drawImage($video, 0, 0);
};

About

๐ŸŽถCross-platform music streaming application with Electron

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages