Google Appsscript로 Youtube 검색 웹앱 만들기

소개

스터디장님이신 "스마트대디"님의 강의를 듣고 저도 사례글을 하나 작성하고자 알려주신 프로세스 다음과 같은 Youtube 검색 웹앱을 만들어 보았습니다. 제가 만들고자 하는 Youtube 웹앱은 평소 해외에서 선전하고 있는 한국인 축구선수 골영상이나 하이라이트를 자동으로 보여주는 검색웹앱을 한번 만들어보기로 하였습니다.

진행 방법

사용했던 Tool : aistudio.google.com 과 script.google.com

  1. aistudio.google.com 을 구동시켜서 프롬프트를 아래와 같이 기입해보았습니다.

##해야할일##
Google Appsscript로 YouTube 검색 웹앱 만들어볼거야. 코드 제공해줘. 
(YouTube Data API → 웹앱)
## 시나리오 ##
1. 나는 YouTube 크리에이터로서 관심 키워드를 손쉽게 검색해보고 싶어. 
2. Google Apps scrip로 웹앱의 검색창에 키워드(해외에서 활약하고 있는 한국 축구선수 이름)을 기입하거나 선택할 수 있게  YouTube Data API를 호출해 조회수가 가장 많이 나오는 영상 5개를 가져오고 
3. 썸네일 · 제목(클릭 시 이동) · 채널명 · 게시일 · 조회수 등을 
4. 화면에 간단히 표시해주는 Google Apps script 코드를 짜줘. (code.gs / index.html 모듈은 딱 2개) 
5. 초보라 최대한 상세하게 설정법/코드를 제공해줘. 
6. 최대한 로직을 간단하게 하기 위해 코드상에 내 API KEY를 직접 넣는 방식으로 짜줘.
'expand 새로운 모델 생각'이라는 단어가있는 화면

Aistudio 가 열심히 생각하고 만들어 주고 있군요...

// Code.gs

// !!! 중요 !!! : 아래 'YOUR_YOUTUBE_API_KEY'를 실제 발급받은 API 키로 교체하세요.
var API_KEY = 'YOUR_YOUTUBE_API_KEY';

// 웹앱 접속 시 index.html 파일을 서비스하는 함수
function doGet(e) {
  return HtmlService.createHtmlOutputFromFile('index.html')
      .setTitle('YouTube 키워드 검색');
}

// YouTube API를 호출하여 비디오를 검색하는 함수 (HTML에서 호출됨)
function searchYouTubeVideos(keyword) {
  if (!keyword) {
    return JSON.stringify({ error: "검색어를 입력해주세요." });
  }

  var searchUrl = 'https://www.googleapis.com/youtube/v3/search?' +
    'part=snippet' +
    '&q=' + encodeURIComponent(keyword) +
    '&type=video' +
    '&order=viewCount' + // 조회수 순으로 정렬
    '&maxResults=15' +   // API는 조회수 정렬 시 약간의 오차가 있을 수 있어 넉넉히 가져와서,
                         // 실제로는 5개만 사용하지만, statistics를 가져오기 위해 ID를 모아야 함
    '&key=' + API_KEY;

  try {
    var searchResponse = UrlFetchApp.fetch(searchUrl, { muteHttpExceptions: true });
    var searchJson = JSON.parse(searchResponse.getContentText());

    if (searchResponse.getResponseCode() !== 200 || searchJson.error) {
      Logger.log("YouTube Search API Error: " + (searchJson.error ? searchJson.error.message : searchResponse.getContentText()));
      return JSON.stringify({ error: "YouTube 검색 API 호출 중 오류가 발생했습니다: " + (searchJson.error ? searchJson.error.message : "응답 코드 " + searchResponse.getResponseCode()) });
    }

    if (!searchJson.items || searchJson.items.length === 0) {
      return JSON.stringify({ items: [] }); // 검색 결과 없음
    }

    // 검색 결과에서 비디오 ID만 추출
    var videoIds = searchJson.items.slice(0, 5).map(function(item) { // 상위 5개 비디오 ID만 사용
      return item.id.videoId;
    }).join(',');

    if (!videoIds) {
        return JSON.stringify({ items: [] }); // ID 추출 실패 (매우 드문 경우)
    }

    // 추출된 비디오 ID들로 각 비디오의 상세 정보(조회수 포함)를 가져옴
    var videosUrl = 'https://www.googleapis.com/youtube/v3/videos?' +
      'part=snippet,statistics' + // snippet(제목, 채널명 등)과 statistics(조회수)를 가져옴
      '&id=' + videoIds +
      '&key=' + API_KEY;

    var videosResponse = UrlFetchApp.fetch(videosUrl, { muteHttpExceptions: true });
    var videosJson = JSON.parse(videosResponse.getContentText());

    if (videosResponse.getResponseCode() !== 200 || videosJson.error) {
       Logger.log("YouTube Videos API Error: " + (videosJson.error ? videosJson.error.message : videosResponse.getContentText()));
       return JSON.stringify({ error: "YouTube 비디오 정보 API 호출 중 오류가 발생했습니다: " + (videosJson.error ? videosJson.error.message : "응답 코드 " + videosResponse.getResponseCode())});
    }

    var results = videosJson.items.map(function(item) {
      return {
        videoId: item.id,
        title: item.snippet.title,
        thumbnailUrl: item.snippet.thumbnails.medium ? item.snippet.thumbnails.medium.url : item.snippet.thumbnails.default.url,
        channelTitle: item.snippet.channelTitle,
        publishedAt: new Date(item.snippet.publishedAt).toLocaleDateString('ko-KR'), // 한국 날짜 형식
        viewCount: item.statistics.viewCount ? parseInt(item.statistics.viewCount).toLocaleString('ko-KR') : '정보 없음' // 조회수, 한국식 콤마
      };
    });
    
    // API가 order=viewCount를 완벽하게 지키지 않을 수 있으므로, 클라이언트에서 받은 후 다시 한번 정렬
    // (단, statistics.viewCount가 문자열이므로 정수형으로 변환 후 비교)
    results.sort(function(a, b) {
        const viewA = parseInt(a.viewCount.replace(/,/g, ''), 10) || 0;
        const viewB = parseInt(b.viewCount.replace(/,/g, ''), 10) || 0;
        return viewB - viewA;
    });


    return JSON.stringify({ items: results.slice(0,5) }); // 최종 5개만 반환

  } catch (e) {
    Logger.log("Exception in searchYouTubeVideos: " + e.toString());
    return JSON.stringify({ error: "스크립트 실행 중 예외가 발생했습니다: " + e.toString() });
  }
}

Code.js 가 나오고 그 이후로 index.html 이 나오겠죠?

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <title>YouTube 키워드 검색</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        margin: 20px;
        background-color: #f4f4f4;
      }
      .container {
        background-color: #fff;
        padding: 20px;
        border-radius: 8px;
        box-shadow: 0 0 10px rgba(0,0,0,0.1);
      }
      h1 {
        color: #333;
        text-align: center;
      }
      .search-area {
        display: flex;
        gap: 10px;
        margin-bottom: 20px;
      }
      #keywordInput {
        flex-grow: 1;
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 4px;
        font-size: 16px;
      }
      #searchButton, .preset-button {
        padding: 10px 15px;
        background-color: #FF0000; /* YouTube Red */
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        font-size: 16px;
      }
      #searchButton:hover, .preset-button:hover {
        background-color: #cc0000;
      }
      #loading {
        text-align: center;
        font-size: 18px;
        color: #555;
        display: none; /* Initially hidden */
      }
      #results {
        margin-top: 20px;
      }
      .video-item {
        display: flex;
        border: 1px solid #eee;
        margin-bottom: 15px;
        padding: 10px;
        border-radius: 4px;
        background-color: #f9f9f9;
      }
      .video-item img {
        width: 160px; /* Medium thumbnail size */
        height: 90px;
        margin-right: 15px;
        object-fit: cover;
        border-radius: 4px;
      }
      .video-info {
        flex-grow: 1;
      }
      .video-info h3 {
        margin-top: 0;
        margin-bottom: 5px;
        font-size: 1.1em;
      }
      .video-info h3 a {
        color: #0000EE; /* Visited link color could be different */
        text-decoration: none;
      }
      .video-info h3 a:hover {
        text-decoration: underline;
      }
      .video-info p {
        margin: 3px 0;
        font-size: 0.9em;
        color: #555;
      }
      .error-message {
        color: red;
        font-weight: bold;
        text-align: center;
      }
      .preset-buttons {
        margin-bottom: 20px;
        text-align: center;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>YouTube 영상 검색</h1>

      <div class="search-area">
        <input type="text" id="keywordInput" placeholder="예: 손흥민, 이강인, 김민재">
        <button id="searchButton" onclick="performSearch()">검색</button>
      </div>

      <div class="preset-buttons">
        <p><strong>추천 검색어 (해외파 축구선수):</strong></p>
        <button class="preset-button" onclick="searchWithPreset('손흥민')">손흥민</button>
        <button class="preset-button" onclick="searchWithPreset('이강인')">이강인</button>
        <button class="preset-button" onclick="searchWithPreset('김민재')">김민재</button>
        <button class="preset-button" onclick="searchWithPreset('황희찬')">황희찬</button>
      </div>
      
      <div id="loading">검색 중...</div>
      <div id="results"></div>
    </div>

    <script>
      function performSearch() {
        var keyword = document.getElementById('keywordInput').value.trim();
        if (!keyword) {
          displayError("검색어를 입력해주세요.");
          return;
        }
        
        document.getElementById('loading').style.display = 'block';
        document.getElementById('results').innerHTML = ''; // 이전 결과 지우기
        
        google.script.run
          .withSuccessHandler(handleSuccess)
          .withFailureHandler(handleFailure)
          .searchYouTubeVideos(keyword);
      }

      function searchWithPreset(presetKeyword) {
        document.getElementById('keywordInput').value = presetKeyword;
        performSearch();
      }

      function handleSuccess(responseString) {
        document.getElementById('loading').style.display = 'none';
        var response = JSON.parse(responseString);

        if (response.error) {
          displayError("오류: " + response.error);
          return;
        }

        var videos = response.items;
        var resultsDiv = document.getElementById('results');
        resultsDiv.innerHTML = ''; // Clear previous results

        if (videos && videos.length > 0) {
          videos.forEach(function(video) {
            var videoDiv = document.createElement('div');
            videoDiv.className = 'video-item';

            var thumbnailUrl = video.thumbnailUrl || 'https://via.placeholder.com/160x90.png?text=No+Thumbnail';

            videoDiv.innerHTML = `
              <img src="${thumbnailUrl}" alt="썸네일">
              <div class="video-info">
                <h3><a href="https://www.youtube.com/watch?v=${video.videoId}" target="_blank">${escapeHtml(video.title)}</a></h3>
                <p><strong>채널:</strong> ${escapeHtml(video.channelTitle)}</p>
                <p><strong>게시일:</strong> ${escapeHtml(video.publishedAt)}</p>
                <p><strong>조회수:</strong> ${escapeHtml(video.viewCount)}</p>
              </div>
            `;
            resultsDiv.appendChild(videoDiv);
          });
        } else {
          resultsDiv.innerHTML = '<p style="text-align:center;">검색 결과가 없습니다.</p>';
        }
      }

      function handleFailure(error) {
        document.getElementById('loading').style.display = 'none';
        displayError("스크립트 실행 실패: " + error.message);
        console.error("Failure: ", error);
      }

      function displayError(message) {
        document.getElementById('loading').style.display = 'none';
        var resultsDiv = document.getElementById('results');
        resultsDiv.innerHTML = `<p class="error-message">${escapeHtml(message)}</p>`;
      }

      // HTML 태그를 이스케이프하는 함수 (XSS 방지)
      function escapeHtml(unsafe) {
        if (typeof unsafe !== 'string') {
            return unsafe; // 문자열이 아니면 그대로 반환
        }
        return unsafe
             .replace(/&/g, "&")
             .replace(/</g, "<")
             .replace(/>/g, ">")
             .replace(/"/g, """)
             .replace(/'/g, "'");
      }

      // Enter 키로 검색 실행
      document.getElementById('keywordInput').addEventListener('keypress', function(event) {
        if (event.key === 'Enter') {
          event.preventDefault(); // Form submit 방지 (만약 form 태그 안에 있다면)
          performSearch();
        }
      });

    </script>
  </body>
</html>

아~~ 길다... 어쨌든 html 까지 나왔습니다 .

계속된 설명까지 추가해봅니다.

한국어는 종이에 표시됩니다.

자자... 그럼 script.google.com 에 코드를 넣어봐야겠죠?

오오... 저장하고 실행이 단번에 됩니다..... 이렇게 빨리 되니 신이 나죠? ㅎㅎ

배포를 선택해서 실행해봅니다. 헉 검색어를 넣어도 추천어를 선택해도 유튜브영상이 검색이 되지 않네요..... 에잇!

Aistudio.google.com 한테 해당내용을 수정해달라고 하고 다시 code.js 와 index.html 개선코드를 다시 구글스크립트에 넣어봤습니다.

그러나.... 에러가 계속나서 스터디장님이 알려주신 것대로 다시 원복하고 검색어를 아래와 같이 변경해보았습니다.

ㅎㅎㅎ

결과와 배운 점

  1. Aistudio 에서도 디버깅 또는 에러 늪에 빠지면 답이 없다.

  2. 왜냐하면 사람은 깊은 빡침때문에 같은놈에게만 시키게 된다.

  3. 향후에는 aistudio 에서 나온 에러 개선을 하기 위해서 다른 AI Tool도 사용해봐야겠다.

    쿨록.....

도움 받은 글 (옵션)

스터디장님 1주차 오프라인 강의자료

👉 이 게시글도 읽어보세요