Topzl์ ๊ด๊ณ ์๋ ๋ฌด๋ฃ ์์
์คํธ๋ฆฌ๋ฐ์ ์ํ ๋ฐ์คํฌํ ์ดํ๋ฆฌ์ผ์ด์
์
๋๋ค. ์ต์ Electron ๋ฒ์ ๊ณผ electron-vite๋ฅผ ๊ธฐ๋ฐ์ผ๋ก UI๋ React ํ๊ฒฝ๊ณผ ํตํฉํ์ฌ ๊ฐ๋ฐ ๋์์ต๋๋ค. ํฌ๋ก์ค ํ๋ซํผ ์ง์์ ํตํ ํธํ์ฑ๊ณผ ์๋ฆ๋ค์ด ์ธํฐํ์ด์ค ๋ฐ ๋ค์ํ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ๊ฒ์ ๋ชฉํ๋ก ํฉ๋๋ค.
์ํํธ์จ์ด ๋ค์ด๋ก๋๋ ์ ์ฅ์์ Releases ํ์ด์ง์์ ํ์ธํ ์ ์์ต๋๋ค. ํ์ฌ๋ Windows ํ๊ฒฝ์์๋ง ์์ ์ ์ผ๋ก ๋์ํ๊ธฐ์ Windows ์ ์ฉ์ผ๋ก ์ ๊ณต๋ฉ๋๋ค.
notice: ํ์ฌ ๋ฆด๋ฆฌ์ฆ ๋ฒ์ ์
Windows๋ง์ ์ ๊ณตํ์ง๋ง ํด๋ผ์ด์ธํธ ํจํค์ง์ ํตํด์ ๋ค๋ฅธ ํ๋ซํผ์์ ์ง์ ํจํค์ง์ด ๊ฐ๋ฅํฉ๋๋ค. ๋ค๋ง ์ด ๊ฒฝ์ฐ ๋ฉ๋ด, ํธ๋ ์ด ๋ฑ ์ผ๋ถ ๊ธฐ๋ฅ์์ ์ฐจ์ด๊ฐ ์์ ์ ์์ต๋๋ค.
์ด ํ๋ก์ ํธ๋ ็ซๅคด็ซ/MusicFreePlugins ์ ์ฅ์์ audiomack ํ๋ฌ๊ทธ์ธ์ ์ฌ์ฉํ์ฌ ์์์ ์ฌ์ URL์ ๊ฐ์ ธ์ต๋๋ค. ๋ด๋ถ์ ์ผ๋ก๋ audiomack api๋ฅผ ์ฌ์ฉํ์ง๋ง ์ ๋ฃ ์ปจํ
์ธ ๋ ํํฐ๋ง๋์ด ์์ต๋๋ค. ์ด ํ๋ฌ๊ทธ์ธ์ ํ์ต ๋ฐ ์ฐธ๊ณ ์ฉ๋๋ก๋ง ์ ๊ณต๋๋ฉฐ, ์์
์ ์ฉ๋๋ก ์ฌ์ฉํ์ง ์์์ผ ํ๊ณ , ๋ฐ๋์ ํฉ๋ฒ์ ์ผ๋ก ์ฌ์ฉํด์ผ ํ๋ค๊ณ ์ ์๋์ด ์์ต๋๋ค.
Topzl์ญ์ ๋์ผํ ๋ชฉ์ ์ผ๋ก ๊ฐ๋ฐ๋ ํ๋ก์ ํธ ์
๋๋ค. ํ๋ก์ ํธ ์ฌ์ฉ ๊ณผ์ ์์ ์ ์๊ถ์ด ์๋ ๋ฐ์ดํฐ๊ฐ ์์ฑ๋ ์ ์์ผ๋ฏ๋ก ์ด์ ๋ํ ์ฃผ์๊ฐ ํ์ํฉ๋๋ค. ๋ํ, ํ์ฌ ๊ฐ์ธ์ฉ ๋ฐ ํ์ต์ฉ์ผ๋ก๋ง ์ฌ์ฉ์ ๊ถ์ฅํ๋ฉฐ, ์ฝ๋ ์ฌ์ด๋ ์์ด ๋ฐฐํฌ๋๊ณ ์๊ธฐ์ ์ค์น ์ ์ด์ ์ฒด์ ์์ ๊ฒฝ๊ณ ๋ฉ์์ง๊ฐ ํ์๋ ์ ์์ต๋๋ค.
- ํฌ๋ก์ค ํ๋ซํผ ์ง์ (Windows, macOS, Linux)
- ์์ , ์จ๋ฒ, ์ํฐ์คํธ, ํ๋ ์ด๋ฆฌ์คํธ ๊ฒ์
- ์์ฒด ์คํธ๋ฆฌ๋ฐ
- ๋ก์ปฌ ์์ ์ฌ์ ์ง์
- ์์ ๋ค์ด๋ก๋ ์ง์
- ์์ปค ์ค๋ ๋๋ฅผ ํ์ฉํ ๋ก์ปฌ ํด๋ ๋ชจ๋ํฐ๋ง ๋ฐ ๋๊ธฐํ
- ๊ฐ์ฌ ์ง์ (์น ํฌ๋กค๋ง ๊ธฐ๋ฐ, ์ ํ๋ ๋ถ์์ )
- ๋ก๊ทธ์ธ ์์ด ์ฌ์ฉ ๊ฐ๋ฅ (์คํ ๋ฆฌ์ง ๋ฐ AppData์ ์ฌ์ฉ์ ๋ฐ์ดํฐ ์ ์ฅ)
- ๋ค๊ตญ์ด ์ง์ (ํ๊ตญ์ด, ์์ด)
- ์ฌ์ฉ์ ์ง์ ๋จ์ถํค ์ง์ (In-App, Global)
- ์ธ๋ถ ์ค์ ์ง์ (์ผ๋ฐ, ์ฌ์, ๋ค์ด๋ก๋, ๊ฐ์ฌ, ๋ฐฑ์ ๋ฐ ๋ณต์)
- PIP ๋ชจ๋ ์ง์
# ์ ์ฅ์ ํด๋ก
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]๐ ๋ชฉ๋ก
- Electorn์ ๊ธฐ๋ณธ ๊ตฌ์กฐ ๋ฐ ๋์์๋ฆฌ
- ๋ค์ค ์๋์ฐ๊ฐ ํต์
- ์์ปค ์ค๋ ๋
- ํ๋ฌ๊ทธ์ธ (Audiomack)
- ๊ฐ์ฌ ๊ฒ์
- ๊ฐ์ ์คํฌ๋กค (useVirtualScroll)
- ์ฌ์๋ชฉ๋ก ์ ๋ ฌ (Drag & Drop)
- Scroll Navigator
- Focus์ Blur ์ด๋ฒคํธ ํ๋ฆ ์ ์ด
- ์ปจํ ์คํธ ๋ฉ๋ด
- EventEmitter
- ๋จ์ถํค ๋ฑ๋ก (In-App, Global)
- ๋ก์ปฌ ๋ฐ์ดํฐ ๊ด๋ฆฌ
- ํ๋ฉด ์บก์ฒ
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 ํต์ ์ ํตํด ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ๊ณ ํ๋ฉด์ ์ ๋ฐ์ดํธํ๋ ๋ฐฉ์์ผ๋ก ๋์ํฉ๋๋ค.
๋ฉ์ธ ํ๋ก์ธ์ค๋ ์ฌ๋ฌ ๊ฐ์ ์๋์ฐ๋ฅผ ์์ฑํ ์ ์์ผ๋ฉฐ ๊ฐ ๋ ๋๋ฌ ํ๋ก์ธ์ค๋ ๋ฉ์ธ ํ๋ก์ธ์ค์์ ํต์ ์ ํฉ๋๋ค. ํ์ง๋ง IPCํต์ ์ผ๋ก๋ ๋ฉ์ธ๊ณผ ๋ ๋๋ฌ๊ฐ์ ์ํต๋ง ๊ฐ๋ฅํ๊ธฐ์ ๋ ๋๋ฌ ํ๋ก์ธ์ค๊ฐ ์ํ๋ฅผ ๊ณต์ ํ๊ธฐ ์ํด์๋ ๋ฉ์ธ ํ๋ก์ธ์ค์์ ์ํ๋ฅผ ์ค๊ฐํด์ผ๋ง ํฉ๋๋ค.
ํ์ฌ 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์ ์ํ๋ฅผ ๋ฐ๊พธ๊ฒ ๋ฉ๋๋ค.
Topzl ์์๋ ๋ก์ปฌ ํ์ผ ๋ชจ๋ํฐ๋ง์ ์ํ FileWatcher ์์ปค์ ๋ค์ด๋ก๋๋ฅผ ์งํํ๊ณ ๊ทธ ๋ค์ด๋ก๋์ ์ํ๋ฅผ ์ค์๊ฐ์ผ๋ก ๋ ๋๋ฌ์๊ฒ ์ ๋ฌํด ์ฃผ๋ Download ์์ปค ์ด๋ ๊ฒ 2๊ฐ์ง ์์ปค ์ค๋ ๋๋ฅผ ์ฌ์ฉํฉ๋๋ค. Comlink ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ๋ฉ์ธ ์ค๋ ๋์ ์์ปค ์ค๋ ๋๊ฐ์ ์ํธ์์ฉ์ ๋ฉ์๋๋ฅผ ํธ์ถํ๋ ๋ฐฉ์์ผ๋ก ๋ ๊ฐ๊ฒฐํ๊ฒ ์์ฑํ์์ต๋๋ค.
์์ ๋ค์ด๋ก๋ ๋ํ ์์ปค ์ค๋ ๋์์ ์คํ๋ฉ๋๋ค. ๋ค์ด๋ก๋๊ฐ ์์๋๋ฉด ๋ ๋๋ฌ ํ๋ก์ธ์ค๋ก ๋ถํฐ ์ ๋ฌ๋ฐ์ 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 });
}
});
}๋ก์ปฌ ํ์ด์ง์์ ํด๋๋ฅผ ๋ฑ๋กํ๊ณ ๊ทธ ํด๋์์ ์ค๋์ค ํ์ผ์ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์ ๋ถ ๋ฝ์์ ๋ฆฌ์คํธ๋ก ๋ณด์ฌ์ค๋๋ค. ์ด ๊ณผ์ ์์ fs๋ชจ๋์ watch ๋ฉ์๋์ ์ ์ฌํ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ chokidar ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ํ์ผ ์์คํ
๊ฐ์ง๋ฅผ ์ ๊ณตํฉ๋๋ค. ์ด๋ฅผ ํตํด ๋ฑ๋กํ ํด๋์ ๋ณํ๊ฐ ์ผ์ด๋๋ฉด ๊ทธ ๋ณํ๋ฅผ ๋ ๋๋ฌ ํ๋ก์ธ์ค์ ์ ๋ฌํด์ ํ๋ฉด์ ์
๋ฐ์ดํธํ๊ณ ํด๋ ์์ฒด์ ๊ฒฝ๋ก๊ฐ ๋ฐ๋๋ฉด ๋ชจ๋ํฐ๋ง์ ๋ค์ ์์ ํฉ๋๋ค.
์์
์ ์ฌ์ํ๋ ค๋ฉด ์ค์ ์ค๋์ค ํ์ผ์ ์ ๊ณตํ๋ 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}๊ธฐ๋ณธ์ ์ผ๋ก 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]
// ์ ๋ฐ ๊ฒ์: ์ฒซ๋ฒ์งธ ๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ์ค์ผ๋ก ํฌ๋กค๋ง ํ, ๋ฒ์ญ๋ ๊ฐ์ฌ๋ฅผ ์ฐพ๊ณ ๋ฐํ
}ํด๋น ์์ ์ด ๋ฒ์ญ๋ ๊ฐ์ฌ๋ฅผ ์ง์ํ์ง ์์ ์๋ ์์๋ฟ๋๋ฌ ํจ์จ์ฑ์ ์ํด์ ์ ํ๋๊ฐ ๊ฐ์ฅ ๋์ ์ ์๋ ์ฒซ ๋ฒ์งธ ๊ฒ์ ๊ฒฐ๊ณผ๋ง ํ์ธ ํ๊ธฐ ๋๋ฌธ์ ๊ฐ๋ฅํ ๋ก๋ง์ ํ๊ธฐ๋ฅผ ํผํ๋ ์์ผ๋ก ๊ฒ์์ ํ์ง๋ง ์ ํ๋๊ฐ ๋จ์ด์ง ์ ์์ต๋๋ค.
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์์น๋ฅผ ๊ธฐ์ค์ผ๋ก ๋ ๋๋ง ๋ฉ๋๋ค.
์ ์ฒด ๋์ด๋ฅผ ์ค์ ํ๊ณ ๋ ๋๋ง์ ์ ํํ ๊ฐ์๋งํผ ํญ๋ชฉ๋ค์ ํ๋ฉด์ ๋ ๋๋งํ์ฌ, ๋ง์ ๋ฐ์ดํฐ๊ฐ ์์๊ฒฝ์ฐ ํ๋ฉด์ ํ ๋ฒ์ ๋ ๋๋ง๋๋ ํญ๋ชฉ์ ์ ํํจ์ผ๋ก์จ ํจ์จ์ ์ผ๋ก ๋ ๋๋ง ํ ์ ์์ต๋๋ค.
์ฌ์๋ชฉ๋ก ๋ฐ ์ฌ์๋ชฉ๋ก ํ ์ด๋ธ์์ ๊ฐ ํญ๋ชฉ์ ๋๋๊ทธ ๋๋์ผ๋ก ์์น๋ฅผ ๋ฐ๊พธ๋ ๊ธฐ๋ฅ์ ์ง์ํฉ๋๋ค.
position: absolute์์ฑ์ ์ฌ์ฉํ์ฌ ๊ฐ ํญ๋ชฉ์ ์ ๋๋ ์๋์ ๋๋กญ ๊ฐ๋ฅํ ์์Droppable์์ญ์ ์์ฑํฉ๋๋ค.- ์ฌ์ฉ์๊ฐ ํญ๋ชฉ์ ๋๋๊ทธ๋ฅผ ์์ํ๋ฉด
dataTransfer๋ฅผ ํ์ฉํด ๋๋๊ทธ๊ฐ ์์๋ ํญ๋ชฉ์index๋ฅผ ์ ์ฅํฉ๋๋ค. 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๋ฅผ ์ฌ์ฉํด์ผ ๊ฐ์ ์คํฌ๋กค์ ์ฌ์ฉํ๋ฉด์ ์ ์์ ์ผ๋ก ๋๋๊ทธ ์ค ๋๋๊ธฐ๋ฅ์ด ๋์ํฉ๋๋ค.
์ฌ์ฉ์๊ฐ ์คํฌ๋กคํ ๋ ํ์ฌ ๋ณด๊ณ ์๋ ์น์
์ ์๋์ผ๋ก ๊ฐ์งํ์ฌ ๋ค๋น๊ฒ์ด์
UI๋ฅผ ์
๋ฐ์ดํธํ๋ ์ธํฐํ์ด์ค๋ฅผ Scrollspy๋ผ๊ณ ํฉ๋๋ค. ์ด ๊ณผ์ ์์ IntersectionObserver๋ฅผ ์ฌ์ฉํ๋ฉด ์ฝ๊ฒ ๊ตฌํํ ์ ์์ต๋๋ค.
- ํ์ฌ ์ ํ๋ ์น์
์ ๊ด๋ฆฌํ๋
state๋ฅผ ์์ฑํฉ๋๋ค. - ๊ฐ ์น์
์ ๋งํฌ์
ํ ๋ ๊ณ ์ ํ
id๋ฅผ ๋ถ์ฌํฉ๋๋ค. IntersectionObserver๋ฅผ ์ฌ์ฉํ์ฌ ๊ฐ ์น์ ์ด ๋ทฐํฌํธ์ ๊ต์ฐจ๋๋ ๋น์จ์ ์ถ์ ํฉ๋๋ค.- ๊ฐ์ฅ ๋ง์ด ๊ต์ฐจ๋ ์น์
์
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๋ฅผ ์
๋ฐ์ดํธํ ์ ์์ต๋๋ค
์๋จ ๋ฉ๋ด๋ ํต์์น๋ฅผ ์ํ 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 ๋ฅผ ์ฌ์ฉํ์ฌ ํฌ์ปค์ฑ์ด ๊ฐ๋ฅํ๋๋ก ํฉ๋๋ค.
์ฌ์๋ชฉ๋ก์์ ๋ง์ฐ์ค ์ฐํด๋ฆญ ์ ์ฌ์๋ชฉ๋ก์ ์จ๋ฒ ์ปค๋ฒ์ ํจ๊ป ์ถ๊ฐ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ์ปจํ ์คํธ ๋ฉ๋ด๊ฐ ๋ํ๋ฉ๋๋ค. ์ด๋ ์ปจํ ์คํธ ๋ฉ๋ด๋ ํ์ฌ ๋ง์ฐ์ค์ ์์น์ ๋ฐ๋ผ์ ๋ฉ๋ด๋ฅผ ์ด๋ ๋ฐฉํฅ์ผ๋ก ๋ณด์ฌ์ค์ผ ํ ์ง ์ ํ๋ผ์ผ ํฉ๋๋ค. ๋ง์ฐ์ค๊ฐ ์ฐ์ธก ์๋จ์์ ์ปจํ ์คํธ ๋ฉ๋ด๊ฐ ์ด๋ฆฐ๋ค๋ฉด ๋ฉ๋ด๊ฐ ํ๋ฉด์์ ๊ฐ๋ ค์ง๋ค๊ฑฐ๋ ์คํฌ๋กค์ด ๋ฐ์ํ ์ ์๊ธฐ ๋๋ฌธ์ ๋๋ค.
// 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์ด๋ฒคํธ๊ฐ ๋ฐ์ํ์ ๋ ๋ง์ฐ์ค์ ์ขํ๋ฅผ ๊ณ์ฐํ์ฌ ํ์ฌ ๋ทฐํฌํธ์์์ ์์น๋ฅผ ๊ธฐ์ค์ผ๋ก ๋ฐ๋ ๋ฐฉํฅ์ผ๋ก ๋ฉ๋ด๊ฐ ์ด๋ฆด ๋ฐฉํฅ์ ๊ฒฐ์ ํ๊ณ ์ด๋ฅผ ํตํด ํญ์ ํ๋ฉด ๋ด์์ ๋ฉ๋ด๊ฐ ํ์๋๋๋ก ๋ณด์ฅํ ์ ์์ต๋๋ค.
์์
์ฌ์๊ณผ ๊ด๋ จ๋ ์ด๋ฒคํธ ๋ฐ ๋จ์ถํค ์
๋ ฅ ์ด๋ฒคํธ ์ฒ๋ฆฌ๋ฅผ ์ํด 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 ์ด๋ฒคํธ๋ฅผ ๋ฐ์์ํค๊ณ , ์ด๋ฅผ ํธ๋ค๋ฌ์์ ์ฒ๋ฆฌํ๋ ๋ฐฉ์์
๋๋ค. ์ฑ์ด ์์๋ ๋ ๋ฏธ๋ฆฌ ๊ฐ ์ด๋ฒคํธ์ ๋ํ ํธ๋ค๋ฌ๋ฅผ ๋ง๋ค์ด๋๊ณ ์ดํ์ ํ๋ ์ด์ด์์ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ฉด ์ง์ ์ฒ๋ฆฌํ๋๊ฒ ์๋ ๋
๋ฆฝ์ ์ธ ์ด๋ฒคํธ ์์คํ
์ ํ์ฉํ์ฌ ์ด๋ฒคํธ๋ค์ ํ๊ณณ์์ ๊ด๋ฆฌํ๋ฉฐ ์ฝ๋์ ์ ์ง๋ณด์์ฑ์ ๋์ผ ์ ์์ต๋๋ค.
์ค์ ํ์ด์ง์์ ์ฌ์ฉ์๊ฐ ์ง์ ๋จ์ถํค๋ฅผ ์ปค์คํ ํ ์ ์์ต๋๋ค.
- In-App ๋จ์ถํค: ์ดํ๋ฆฌ์ผ์ด์ ์ด ํฌ์ปค์ค๋ ์ํ์์๋ง ๋์
- Global ๋จ์ถํค: ๋ฐฑ๊ทธ๋ผ์ด๋์์ ๋ค๋ฅธ ์ดํ๋ฆฌ์ผ์ด์ ์ฌ์ฉ ์ค์๋ ๋์
In-App๋จ์ถํค๋ hotkeys-js๋ฅผ ์ฌ์ฉํด์ ๋ ๋๋ฌ ํ๋ก์ธ์ค์์ ๊ด๋ฆฌํ๋ฉฐ, Global ๋จ์ถํค๋ ๋ฉ์ธ ํ๋ก์ธ์ค์์ electron ๋ชจ๋์ globalShortcut API๋ฅผ ์ฌ์ฉํ์ฌ ์์คํ
์ ๋ฑ๋กํฉ๋๋ค. ๋ด๋ถ์ ์ผ๋ก๋ EventEmitter์ ์ฌ์ฉํ์ฌ ๊ฐ ๊ธฐ๋ฅ๋ค์ ๋ํ ํธ๋ค๋ฌ๋ฅผ ๋ฑ๋กํด ๋๊ณ ์ดํ ์ฌ์ฉ์๊ฐ ํน์ ๋จ์ถํค๋ฅผ ์ค์ ํ๋ฉด, keydown ์ด๋ฒคํธ ๋ฐ์ ์ ํด๋น ํธ๋ค๋ฌ๊ฐ ์คํ๋๋ ๋ฐฉ์์ผ๋ก ๋์ํฉ๋๋ค.
ํ์ฌ ๋ฉ์ธ ํ๋ก์ธ์ค์ ๋ ๋๋ฌ ํ๋ก์ธ์ค์์ ๋ก์ปฌ ๋ฐ์ดํฐ๋ฅผ ๊ด๋ฆฌํ๊ธฐ ์ํด์ ์ธ ๊ฐ์ง ๋ฐฉ์์ ์ฌ์ฉํฉ๋๋ค.
- JSON ํ์ผ (๋ฉ์ธ ํ๋ก์ธ์ค)
- ๋ก์ปฌ ์คํ ๋ฆฌ์ง (๋ ๋๋ฌ ํ๋ก์ธ์ค)
- IndexedDB (๋ ๋๋ฌ ํ๋ก์ธ์ค)
๋ชจ๋ ์ฌ์ฉ์ ์ค์ ์ Windows ํ๊ฒฝ ๊ธฐ์ค์ผ๋ก AppData\Roaming\topzl\config.json ํ์ผ์์ ๊ด๋ฆฌ๋์ด ๋ฉ์ธ ํ๋ก์ธ์ค์ ๋ ๋๋ฌ ํ๋ก์ธ์ค์์ ์ค์ ๋ฐ์ดํฐ๋ฅผ ๊ณต์ ๋ฉ๋๋ค. ํ์ฌ ์ฌ์ ์ค์ธ ๊ณก, ๋ณผ๋ฅจ, ์ฌ์์๋, ๋ฐ๋ณต๋ชจ๋, ์
ํ๋ชจ๋ ๋ฑ์ ๋น๊ต์ ํ๋ฐ์ฑ ๋ฐ์ดํฐ์ ๊ฐ๊น๊ณ ์ข ๋ ๋จ์ํ ํ์
์ ๋ฐ์ดํฐ๋ ์คํ ๋ฆฌ์ง๋ก ๊ด๋ฆฌํฉ๋๋ค. ๋ฐ๋ฉด ํ์ฌ ์ฌ์๋ชฉ๋ก, ์ ์ฒด ์ฌ์๋ชฉ๋ก ๋ฑ์ ์ปฌ๋ ์
๋ฐ์ดํฐ ์ฒ๋ผ ๋์ฉ๋์ด ๋ ์ ์๋ ๋ฐ์ดํฐ๋ IndexedDB๋ฅผ ์ฌ์ฉํ์ฌ ๊ด๋ฆฌํฉ๋๋ค.
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);
};

















