๐ŸŒฑ <groot/>
์œ ํŠœ๋ธŒ ๋Œ“๊ธ€์˜ ํƒ€์ž„๋ผ์ธ์„ ๋‚ด ์žฌ์ƒ๋ชฉ๋ก์œผ๋กœ โ€” ์†์œผ๋กœ ํ•˜๋˜ ๊ฑธ ์ž๋™ํ™”ํ–ˆ๋‹ค | Groot's Tech Blog
โš›๏ธReact

์œ ํŠœ๋ธŒ ๋Œ“๊ธ€์˜ ํƒ€์ž„๋ผ์ธ์„ ๋‚ด ์žฌ์ƒ๋ชฉ๋ก์œผ๋กœ โ€” ์†์œผ๋กœ ํ•˜๋˜ ๊ฑธ ์ž๋™ํ™”ํ–ˆ๋‹ค

2026.07.02ยท4๋ถ„ ์ฝ๊ธฐ

๋…ธ๋ž˜๋Š” ์ข‹์€๋ฐ, ๋ช‡ ๊ณก์ด ๊ฑธ๋ฆฐ๋‹ค

์œ ํŠœ๋ธŒ์—์„œ "Lo-fi 1์‹œ๊ฐ„ ๋ชจ์Œ", "์žฌ์ฆˆ ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ" ๊ฐ™์€ ๊ธด ์˜์ƒ์„ ์ž์ฃผ ๋“ฃ๋Š”๋‹ค. ์ด๋Ÿฐ ์˜์ƒ์€ ๋Œ“๊ธ€์— ๊ผญ ํƒ€์ž„๋ผ์ธ์ด ๋‹ฌ๋ ค ์žˆ๋‹ค.

00:00 ๊ณกA - ๊ฐ€์ˆ˜1
03:24 ๊ณกB - ๊ฐ€์ˆ˜2
07:11 ๊ณกC - ๊ฐ€์ˆ˜3
...

๋ฌธ์ œ๋Š”, ํ•œ ์‹œ๊ฐ„์งœ๋ฆฌ ์•ˆ์— ๋‚ด๊ฐ€ ๋ณ„๋กœ์ธ ๊ณก๋„ ์„ž์—ฌ ์žˆ๋‹ค๋Š” ๊ฒƒ. ๋งˆ์Œ์— ๋“œ๋Š” ๊ณก๋งŒ ๊ณจ๋ผ ๋‚ด ์žฌ์ƒ๋ชฉ๋ก์œผ๋กœ ๋งŒ๋“ค๊ณ  ์‹ถ์€๋ฐ, ๊ทธ๋Ÿฌ๋ ค๋ฉด ๊ณก์„ ํ•˜๋‚˜ํ•˜๋‚˜ ๊ฒ€์ƒ‰ํ•ด์„œ ์žฌ์ƒ๋ชฉ๋ก์— ์ถ”๊ฐ€ํ•ด์•ผ ํ•œ๋‹ค. 20๊ณก์ด๋ฉด 20๋ฒˆ. ์ด๊ฒŒ ์€๊ทผํžˆ ๊ท€์ฐฎ์•˜๋‹ค.

๊ทธ๋ž˜์„œ ๋งŒ๋“ค์—ˆ๋‹ค. ํƒ€์ž„๋ผ์ธ ํ…์ŠคํŠธ๋ฅผ ๋ถ™์—ฌ๋„ฃ์œผ๋ฉด, ์›ํ•˜๋Š” ๊ณก๋งŒ ๊ณจ๋ผ YouTube ์žฌ์ƒ๋ชฉ๋ก์œผ๋กœ ์ž๋™ ์ƒ์„ฑํ•ด์ฃผ๋Š” ์›น์•ฑ. ์ด๋ฆ„์€ YPG(YouTube Playlist Generator).

๋ถ™์—ฌ๋„ฃ๊ธฐ โ†’ ์›ํ•˜๋Š” ๊ณก ์ฒดํฌ โ†’ ์ƒ์„ฑ ๋ฒ„ํŠผ. ๋.

์ „์ฒด ํ๋ฆ„

๋งŒ๋“ค๊ณ  ๋ณด๋‹ˆ ํฐ ํ๋ฆ„์€ ๋„ค ๋‹จ๊ณ„์˜€๋‹ค.

  1. ๋กœ๊ทธ์ธ โ€” Google OAuth๋กœ YouTube ๊ณ„์ • ๊ถŒํ•œ์„ ๋ฐ›๋Š”๋‹ค
  2. ํŒŒ์‹ฑ โ€” ๋ถ™์—ฌ๋„ฃ์€ ํƒ€์ž„๋ผ์ธ์—์„œ "๊ณก๋ช… - ๊ฐ€์ˆ˜"๋งŒ ๋ฝ‘์•„๋‚ธ๋‹ค
  3. ๊ฒ€์ƒ‰ & ์„ ํƒ โ€” ๊ฐ ๊ณก์„ YouTube์—์„œ ๊ฒ€์ƒ‰ํ•˜๊ณ , ์›ํ•˜๋Š” ์˜์ƒ์„ ๊ณ ๋ฅธ๋‹ค
  4. ์ƒ์„ฑ โ€” ์ƒˆ ์žฌ์ƒ๋ชฉ๋ก์„ ๋งŒ๋“ค๊ณ  ๊ณ ๋ฅธ ๊ณก๋“ค์„ ๋‹ด๋Š”๋‹ค

ํ•˜๋‚˜์”ฉ ํ’€์–ด๋ณธ๋‹ค.

1. ์ง€์ €๋ถ„ํ•œ ํ…์ŠคํŠธ์—์„œ ๊ณก๋ช…๋งŒ ๋ฝ‘๊ธฐ

์‚ฌ์šฉ์ž๊ฐ€ ๋ถ™์—ฌ๋„ฃ๋Š” ํƒ€์ž„๋ผ์ธ์€ ํ˜•์‹์ด ์ œ๊ฐ๊ฐ์ด๋‹ค. 00:00, [1:02:03], 1) ๊ณก๋ช…, 01. ๊ณก๋ช… - ๊ฐ€์ˆ˜โ€ฆ ์—ฌ๊ธฐ์„œ ํƒ€์ž„์Šคํƒฌํ”„์™€ ํŠธ๋ž™ ๋ฒˆํ˜ธ๋ฅผ ๊ฑท์–ด๋‚ด๊ณ  ๊ณก๋ช…๋งŒ ๋‚จ๊ฒจ์•ผ ๊ฒ€์ƒ‰์ด ๋œ๋‹ค. ์ •๊ทœ์‹์œผ๋กœ ์ฒ˜๋ฆฌํ–ˆ๋‹ค.

const makeList = (songList: string[]) =>
  songList.map((value) =>
    value
      // [00:00], (1:02:03), 00:00, 1:02:03 ํ˜•ํƒœ์˜ ํƒ€์ž„์Šคํƒฌํ”„ ์ œ๊ฑฐ
      .replace(/[[(]?\s*\d{1,2}:\d{2}(?::\d{2})?\s*[)\]]?/g, " ")
      // ์•ž๋จธ๋ฆฌ์˜ "1.", "01)", "1)" ๊ฐ™์€ ํŠธ๋ž™ ๋ฒˆํ˜ธ ์ œ๊ฑฐ
      .replace(/^\s*\d{1,3}[.)]\s+/, " ")
      // ์—ฐ์† ๊ณต๋ฐฑ ์ •๋ฆฌ
      .replace(/\s+/g, " ")
      .trim()
  );

๋ถ™์—ฌ๋„ฃ์€ ๋ฌธ์ž์—ด์„ ์ค„ ๋‹จ์œ„๋กœ ์ชผ๊ฐœ๊ณ , ์œ„ ํ•จ์ˆ˜๋กœ ์ •๋ฆฌํ•œ ๋’ค Set์œผ๋กœ ์ค‘๋ณต์„ ์ œ๊ฑฐํ•œ๋‹ค. (๊ฐ™์€ ๊ณก์ด ๋‘ ๋ฒˆ ์ ํžŒ ํƒ€์ž„๋ผ์ธ๋„ ํ”ํ•˜๋‹ค.)

setSongList([...new Set(makeList(raw.split("\n")))]);

์—ฌ๊ธฐ์„œ ๋ฐฐ์šด ๊ฒƒ ํ•˜๋‚˜: ์ฒ˜์Œ์—” ๊ณต๋ฐฑ์„ join(" ")์œผ๋กœ ๋Œ€์ถฉ ์ด์–ด๋ถ™์˜€๋Š”๋ฐ, ๊ทธ๋Ÿฌ๋ฉด "๊ณก๋ช…๊ฐ€์ˆ˜"์ฒ˜๋Ÿผ ๋ถ™์–ด๋ฒ„๋ ค์„œ ๊ฒ€์ƒ‰ ์ •ํ™•๋„๊ฐ€ ๋–จ์–ด์กŒ๋‹ค. ๊ณต๋ฐฑ์„ ์‚ด๋ฆฌ๋Š” ์ •๊ทœ์‹ ์น˜ํ™˜์œผ๋กœ ๋ฐ”๊พธ๋‹ˆ ๊ฒ€์ƒ‰์ด ํ›จ์”ฌ ์ž˜ ๋๋‹ค. ์‚ฌ์†Œํ•ด ๋ณด์ด๋Š” ์ „์ฒ˜๋ฆฌ๊ฐ€ ๊ฒฐ๊ณผ ํ’ˆ์งˆ์„ ์ขŒ์šฐํ–ˆ๋‹ค.

2. Google OAuth๋กœ YouTube ๊ถŒํ•œ ๋ฐ›๊ธฐ

์žฌ์ƒ๋ชฉ๋ก์„ ๋งŒ๋“ค๋ ค๋ฉด ์‚ฌ์šฉ์ž์˜ YouTube ๊ณ„์ • ๊ถŒํ•œ์ด ํ•„์š”ํ•˜๋‹ค. Google OAuth 2.0 Authorization Code ๋ฐฉ์‹์„ ์ผ๋‹ค.

๋จผ์ € ๋™์˜ ํ™”๋ฉด์œผ๋กœ ๋ณด๋‚ธ๋‹ค. access_type=offline์„ ์ฃผ๋ฉด refresh token์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด์„œ, ๋งค๋ฒˆ ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค.

const REDIRECT_URI = `${window.location.origin}/create`;
const SCOPE = "https://www.googleapis.com/auth/youtube";
const url =
  `https://accounts.google.com/o/oauth2/auth` +
  `?prompt=consent&access_type=offline` +
  `&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}` +
  `&response_type=code&scope=${SCOPE}`;

๋™์˜ํ•˜๋ฉด ?code=...๋ฅผ ๋‹ฌ๊ณ  /create๋กœ ๋Œ์•„์˜จ๋‹ค. ์ด code๋ฅผ access token์œผ๋กœ ๊ตํ™˜ํ•˜๊ณ , refresh token์€ ์ฟ ํ‚ค์— ์ €์žฅํ•ด๋‘”๋‹ค.

const token = await getAccessToken({ code: auth });
setCookie("refresh_token", token.data.refresh_token, { /* ... */ });

๋‹ค์Œ๋ถ€ํ„ฐ๋Š” ์ฟ ํ‚ค์˜ refresh token์œผ๋กœ access token์„ ์ƒˆ๋กœ ๋ฐœ๊ธ‰๋ฐ›๋Š”๋‹ค.

const refresh_token = getCookie("refresh_token");
const accessToken = await getAccessTokenByRefreshToken({ refresh_token });

๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI๋ฅผ window.location.origin ๊ธฐ์ค€์œผ๋กœ ๋™์ ์œผ๋กœ ๋งŒ๋“  ๊ฒŒ ํฌ์ธํŠธ๋‹ค. ๋กœ์ปฌ(localhost:3000)๊ณผ ๋ฐฐํฌ(vercel) ์–‘์ชฝ์—์„œ ์ฝ”๋“œ ์ˆ˜์ • ์—†์ด ๋™์ž‘ํ•œ๋‹ค. (๋Œ€์‹  Google Console์˜ "์Šน์ธ๋œ ๋ฆฌ๋””๋ ‰์…˜ URI"์— ๋‘ ๋„๋ฉ”์ธ์„ ๋‹ค ๋“ฑ๋กํ•ด์•ผ ํ•œ๋‹ค โ€” ์—ฌ๊ธฐ์„œ ํ•œ์ฐธ ํ—ค๋งธ๋‹ค.)

์†”์งํ•œ ํ•œ๊ณ„: ์ด ํ”„๋กœ์ ํŠธ๋Š” ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ง์ ‘ ํ† ํฐ ๊ตํ™˜์„ ํ•˜๋А๋ผ client_secret์ด ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœ๋œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  refresh token๋„ ๋ฌธ์ œ๋‹ค โ€” ์ฝ”๋“œ์—์„œ ์ฟ ํ‚ค์— httpOnly: true ์˜ต์…˜์„ ์คฌ์ง€๋งŒ, HttpOnly๋Š” ์„œ๋ฒ„๊ฐ€ Set-Cookie ํ—ค๋”๋กœ๋งŒ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์–ด์„œ JS์˜ setCookie์—์„œ๋Š” ์กฐ์šฉํžˆ ๋ฌด์‹œ๋œ๋‹ค. ๊ฒฐ๊ตญ refresh token์ด JS๋กœ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ์ฟ ํ‚ค์— ํ‰๋ฌธ์œผ๋กœ ์ €์žฅ๋˜๋Š” ์…ˆ์ด๋ผ XSS์— ๊ทธ๋Œ€๋กœ ๋…ธ์ถœ๋œ๋‹ค. ๊ฐœ์ธ์šฉ ํ† ์ด ํ”„๋กœ์ ํŠธ๋ผ ๋„˜์–ด๊ฐ”์ง€๋งŒ, ์ œ๋Œ€๋กœ ํ•˜๋ ค๋ฉด ํ† ํฐ ๊ตํ™˜๊ณผ ํ† ํฐ ๋ณด๊ด€ ๋ชจ๋‘ **์„œ๋ฒ„(๋˜๋Š” ์„œ๋ฒ„๋ฆฌ์Šค ํ•จ์ˆ˜)**์—์„œ ํ•ด์•ผ ํ•œ๋‹ค. ์ด๊ฑด ๋‹ค์Œ์— ๊ณ ์น  ์ˆ™์ œ๋กœ ๋‚จ๊ฒจ๋’€๋‹ค.

3. ๊ฒ€์ƒ‰ํ•˜๊ณ , ๊ณ ๋ฅด๊ณ , ๋‹ด๊ธฐ

์ฒดํฌํ•œ ๊ณก๋“ค์„ YouTube Data API๋กœ ๊ฒ€์ƒ‰ํ•œ๋‹ค. ๊ณก๋งˆ๋‹ค ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ๋ผ, ๋ชจ๋‹ฌ์„ ๋„์›Œ ์‚ฌ์šฉ์ž๊ฐ€ ์ •ํ™•ํ•œ ์˜์ƒ์„ ๊ณ ๋ฅด๊ฒŒ ํ–ˆ๋‹ค. (๋ผ์ด๋ธŒ ๋ฒ„์ „, ์ปค๋ฒ„, ๋‹ค๋ฅธ ๊ฐ€์ˆ˜์˜ ๋™๋ช… ๊ณก ๋“ฑ์„ ๊ฑธ๋Ÿฌ๋‚ด๊ธฐ ์œ„ํ•ด.)

const promises = checkValue.map(async (song) => {
  const res = await searchVideo({
    key: API_KEY, part: "snippet", q: song, maxResults: 5, type: "video",
  });
  // ๊ฒฐ๊ณผ์—์„œ videoId/์ œ๋ชฉ/์ธ๋„ค์ผ์„ ๋ชจ์•„ ๋ชจ๋‹ฌ์— ๋ณด์—ฌ์ค€๋‹ค
});

๊ณ ๋ฅธ ๊ณก์ด ์ •ํ•ด์ง€๋ฉด ์žฌ์ƒ๋ชฉ๋ก์„ ๋งŒ๋“ค๊ณ  ํ•˜๋‚˜์”ฉ ๋‹ด๋Š”๋‹ค.

// ์ƒˆ ์žฌ์ƒ๋ชฉ๋ก ์ƒ์„ฑ (๊ธฐ๋ณธ ๋น„๊ณต๊ฐœ)
await createNewPlayList(title, accessToken, "private");
// ๊ฐ ์˜์ƒ์„ ์žฌ์ƒ๋ชฉ๋ก์— ์ถ”๊ฐ€
await addToPlayList(accessToken, playlistId, videoId);

๊ณก์ด ๋งŽ์œผ๋ฉด ์‹œ๊ฐ„์ด ๊ฑธ๋ฆฌ๋ฏ€๋กœ progress(done/total) ์ƒํƒœ๋กœ ์ง„ํ–‰๋ฅ ์„ ๋ณด์—ฌ์ค˜์„œ, "๋ฉˆ์ถ˜ ๊ฑฐ ์•„๋ƒ?" ํ•˜๋Š” ๋ถˆ์•ˆ์„ ์—†์•ด๋‹ค.

๊ฐ€์žฅ ํฌ๊ฒŒ ๋ถ€๋”ชํžŒ ๋ฒฝ: API ํ• ๋‹น๋Ÿ‰(quota)

๋งŒ๋“ค๋ฉด์„œ ์ œ์ผ ํฌ๊ฒŒ ๋ฐ์ธ ๋ถ€๋ถ„. YouTube Data API๋Š” ํ•˜๋ฃจ ํ• ๋‹น๋Ÿ‰์ด ์žˆ๋Š”๋ฐ, ๊ทธ๋ƒฅ ์š”์ฒญ ์ˆ˜๊ฐ€ ์•„๋‹ˆ๋ผ ์ž‘์—…๋ณ„ ๋น„์šฉ(unit) ์œผ๋กœ ๊ณ„์‚ฐ๋œ๋‹ค. ํŠนํžˆ ๊ฒ€์ƒ‰(search.list)์ด ์š”์ฒญ๋‹น 100 units๋กœ ์œ ๋… ๋น„์‹ธ๋‹ค. ๊ธฐ๋ณธ ํ• ๋‹น๋Ÿ‰์ด ํ•˜๋ฃจ 10,000 units์ด๋‹ˆ, ๊ฒ€์ƒ‰๋งŒ ํ•˜๋ฉด ํ•˜๋ฃจ 100๋ฒˆ์ด๋ฉด ๋์ด๋‹ค.

๊ณก 20๊ฐœ์งœ๋ฆฌ ํƒ€์ž„๋ผ์ธ ํ•˜๋‚˜๋งŒ ๋Œ๋ ค๋„ ๊ฒ€์ƒ‰ 2,000 units๊ฐ€ ์ˆœ์‹๊ฐ„์— ๋‚˜๊ฐ„๋‹ค. "์™œ ๊ฐ‘์ž๊ธฐ ์•ˆ ๋˜์ง€?" ํ•˜๊ณ  ๋ณด๋ฉด quota ์ดˆ๊ณผ์˜€๋‹ค. ์ด๊ฑธ ๊ฒช๊ณ  ๋‚˜์„œ

  • ๋ถˆํ•„์š”ํ•œ ์žฌ๊ฒ€์ƒ‰์„ ๋ง‰๊ณ (๊ฐ™์€ ๊ณก ์ค‘๋ณต ์ œ๊ฑฐ๊ฐ€ ์—ฌ๊ธฐ์„œ๋„ ๋„์›€์ด ๋๋‹ค),
  • ๊ฐœ๋ฐœ ์ค‘์—” ์‹ค์ œ API๋ฅผ ๊ณ„์† ๋•Œ๋ฆฌ์ง€ ์•Š๋„๋ก MSW(Mock Service Worker)๋กœ ์‘๋‹ต์„ ๋ชฉํ‚นํ•ด์„œ ํ• ๋‹น๋Ÿ‰์„ ์•„๊ผˆ๋‹ค.

"์™ธ๋ถ€ API์—๋Š” ๋ˆˆ์— ์•ˆ ๋ณด์ด๋Š” ๋น„์šฉ๊ณผ ํ•œ๊ณ„๊ฐ€ ์žˆ๋‹ค"๋Š” ๊ฑธ ๋ชธ์œผ๋กœ ๋ฐฐ์šด ํ”„๋กœ์ ํŠธ์˜€๋‹ค.

๋งˆ์น˜๋ฉฐ

๊ณก 20๊ฐœ ร— ๊ฒ€์ƒ‰ + ์ถ”๊ฐ€๋ฅผ ์†์œผ๋กœ ํ•˜๋ฉด 10๋ถ„ ๋„˜๊ฒŒ ๊ฑธ๋ฆฌ๋˜ ์ผ์ด, ๋ถ™์—ฌ๋„ฃ๊ธฐ โ†’ ์ฒดํฌ โ†’ ๋ฒ„ํŠผ ํ•œ ๋ฒˆ์œผ๋กœ ์ค„์—ˆ๋‹ค. ๋‚ด๊ฐ€ ๋งค์ฃผ ๊ฒช๋˜ ์‚ฌ์†Œํ•œ ๊ท€์ฐฎ์Œ ํ•˜๋‚˜๊ฐ€, ์‹ค์ œ๋กœ ์“ฐ๋Š” ๋„๊ตฌ๊ฐ€ ๋œ ์ˆœ๊ฐ„์ด์—ˆ๋‹ค.

โ† ๋ชฉ๋ก์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ
ยฉ 2026 ยท Groot's Tech Blog. Built with Gatsby ๐ŸŒฑ