ch09. JavaScript 실행 -- YouTube 재생목록, ChatGPT 마크다운

브라우저에서 JavaScript를 실행하여 YouTube 추천을 관리하고, 영상 데이터를 마크다운으로 추출하는 다섯 가지 매크로를 다룬다.

이런 불편함, 겪어보셨나요?

YouTube를 오래 쓰다 보면 추천 알고리즘이 이상해진다. 한 번 클릭한 영상 때문에 관심 없는 채널이 계속 올라온다. "관심 없음"을 누르려면 영상마다 점 세 개 메뉴를 열어야 한다. 한두 개면 괜찮지만, 수십 개를 정리하려면 손이 아프다.

재생목록 관리도 마찬가지다. "나중에 볼 동영상"에 쌓인 수백 개를 하나씩 삭제하는 건 고역이다.

반대로, YouTube 영상을 체계적으로 정리하고 싶을 때도 있다. 영상 내용을 마크다운으로 요약해서 Google Docs에 저장하고 싶다. 수동으로 하면 영상 하나에 5분은 걸린다. 재생목록 전체를 정리하려면 하루가 모자란다.

이 챕터에서는 두 가지 문제를 해결한다. YouTube 추천 관리 매크로 두 개, 데이터 추출 매크로 세 개다. 핵심 기술은 ExecuteJavaScriptExecuteShellScript다.

그룹 A: YouTube 추천 관리

매크로 분석: Youtube - Not interested

무엇을 하는 매크로인가

Control+E를 누르면 현재 YouTube 영상에 "관심 없음" 처리를 한다. Chrome 또는 Comet 브라우저에서 동작한다.

트리거

트리거 타입 설명
HotKey Control+E Chrome/Comet에서 YouTube 페이지일 때만 동작

액션 흐름

flowchart TD A(["Control+E 단축키 입력"]) --> B["osascript로 활성 앱 이름 확인"] B --> C{"활성 앱이\nChrome 또는 Comet?"} C -- 아니오 --> D(["종료"]) C -- 예 --> E["osascript로 활성 탭 URL 가져오기"] E --> F{"URL에\nyoutube.com 포함?"} F -- 아니오 --> D F -- 예 --> G["Node.js 스크립트 실행\nnot interested.js"] G --> H(["관심 없음 처리 완료"]) style A fill:#E3F2FD,stroke:#1976D2 style B fill:#E8F5E9,stroke:#388E3C style C fill:#FFF8E1,stroke:#FFA000 style D fill:#E3F2FD,stroke:#1976D2 style E fill:#E8F5E9,stroke:#388E3C style F fill:#FFF8E1,stroke:#FFA000 style G fill:#E8F5E9,stroke:#388E3C style H fill:#E3F2FD,stroke:#1976D2

그림: Youtube - Not interested 매크로의 액션 흐름. 셸 스크립트 내부에서 앱과 URL을 확인한 뒤 Node.js 스크립트를 실행한다.

  1. 활성 앱 확인 -- osascript로 현재 앱이 Chrome인지 Comet인지 확인한다. 둘 다 아니면 종료한다.
  2. URL 확인 -- 활성 탭의 URL을 가져와서 youtube.com이 포함되어 있는지 확인한다. YouTube가 아니면 종료한다.
  3. Node.js 스크립트 실행 -- 외부 JavaScript 파일(not interested.js)을 Node.js로 실행한다.

핵심 기술 해설

ExecuteShellScript와 osascript 조합

이 매크로는 KM의 ExecuteShellScript 액션 하나로 구성된다. 셸 스크립트 안에서 osascript를 호출하여 현재 앱과 URL을 판별한다.

# 현재 활성 앱이 무엇인지 확인합니다.
ACTIVE_APP=$(osascript -e 'tell application "System Events" to name of first application process whose frontmost is true')

if [ "$ACTIVE_APP" != "Google Chrome" ] && [ "$ACTIVE_APP" != "Comet" ]; then
    exit 0
fi

앱 확인 후, 해당 앱에서 URL을 가져오는 부분도 osascript를 사용한다.

if [ "$ACTIVE_APP" = "Google Chrome" ]; then
    CURRENT_URL=$(osascript -e 'tell application "Google Chrome" to try' \
                           -e 'get URL of active tab of front window' \
                           -e 'on error' \
                           -e 'return ""' \
                           -e 'end try')
fi

YouTube 페이지가 맞으면 외부 Node.js 스크립트를 실행한다. Puppeteer 같은 브라우저 자동화 도구를 사용하여 "관심 없음" 버튼을 클릭하는 방식이다.

SCRIPT_PATH="$HOME/Library/CloudStorage/Dropbox/.../not interested.js"
cd "$SCRIPT_DIR" || exit 1
export PATH=/usr/local/bin:/opt/homebrew/bin:$PATH
/usr/local/bin/node "$SCRIPT_PATH"

셸 스크립트에서 조건 분기하는 패턴

KM의 IfThenElse 액션을 쓰지 않고, 셸 스크립트 내부에서 if/elif/fi로 분기한다. 조건이 여러 개일 때 셸 스크립트 하나로 처리하면 액션 수가 줄어든다. 매크로 구조가 단순해지는 장점이 있다.

활용 팁

  • maxDeleteCount 같은 변수를 외부 스크립트에 추가하면 한 번에 여러 영상을 처리할 수 있다.
  • Comet(Perplexity 브라우저)에서도 동작하도록 설계되어 있다. 다른 Chromium 기반 브라우저를 추가하려면 osascript 분기만 추가하면 된다.

매크로 분석: Youtube - Don't recommend channel

무엇을 하는 매크로인가

Control+R을 누르면 현재 YouTube 영상의 채널을 "추천하지 않음" 처리한다. "관심 없음"은 영상 단위, "추천하지 않음"은 채널 단위라는 차이가 있다.

트리거

트리거 타입 설명
HotKey Control+R Chrome/Comet에서 YouTube 페이지일 때만 동작

액션 흐름

이 매크로의 구조는 "Not interested"와 거의 동일하다. 셸 스크립트의 앱 확인, URL 확인 로직이 같다. 차이점은 마지막에 실행하는 Node.js 스크립트가 don't-recommend-channel.js라는 것이다.

SCRIPT_PATH="$HOME/Library/CloudStorage/Dropbox/.../don't-recommend-channel.js"

핵심 기술 해설

같은 구조, 다른 동작

두 매크로는 동일한 셸 스크립트 템플릿을 공유한다. 트리거(Control+E vs Control+R)와 실행 스크립트만 다르다. 이런 패턴은 비슷한 동작을 여러 단축키에 배정할 때 유용하다.

YouTube의 "관심 없음"과 "채널 추천 안 함"은 DOM에서 클릭하는 메뉴 항목이 다를 뿐이다. 외부 Node.js 스크립트에서 해당 메뉴 항목의 텍스트를 찾아 클릭한다.

활용 팁

  • 두 매크로를 합쳐서 하나의 팔레트로 만들 수도 있다. Macro Palette 트리거를 사용하면 "관심 없음" / "채널 추천 안 함"을 선택할 수 있다.
  • 같은 패턴으로 "나중에 볼 동영상에 추가" 같은 YouTube 메뉴 동작도 자동화할 수 있다.

그룹 B: YouTube 데이터 추출

매크로 분석: Youtube - 재생목록에서 목록 삭제

무엇을 하는 매크로인가

YouTube 재생목록 페이지에서 영상을 자동으로 10개씩 삭제한다. 트리거 없이 다른 매크로에서 호출하거나, 수동으로 실행한다.

트리거

트리거 타입 설명
(없음) -- 트리거 없이 수동 실행 또는 서브매크로로 호출

액션 흐름

이 매크로는 ExecuteJavaScript 액션 하나로 구성된다. Chrome 브라우저의 현재 탭에서 JavaScript를 직접 실행한다.

  1. 첫 번째 영상의 메뉴 버튼 탐색 -- ytd-playlist-video-renderer 요소에서 "작업 메뉴" 버튼을 찾는다.
  2. 메뉴 열기 -- pointerdown, mousedown, mouseup, click 이벤트를 순서대로 발생시킨다.
  3. 삭제 버튼 클릭 -- 메뉴에서 "Inbox에서 삭제" 텍스트를 찾아 클릭한다.
  4. 반복 -- 위 과정을 최대 10회 반복한다. 더 이상 항목이 없으면 중단한다.

핵심 기술 해설

ExecuteJavaScript in Browser

KM의 ExecuteJavaScript 액션은 Chrome이나 Safari의 현재 탭에서 JavaScript를 실행한다. 웹페이지의 DOM에 직접 접근할 수 있다. 이 매크로에서는 YouTube 페이지의 재생목록 요소를 조작한다.

(async () => {
  const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
  const maxDeleteCount = 10; // 삭제할 최대 영상 수

  const getFirstMenuButton = () => {
    const items = document.querySelectorAll('ytd-playlist-video-renderer');
    if (!items.length) return null;
    return items[0].querySelector('button[aria-label="작업 메뉴"]');
  };

  const clickDeleteMenu = () => {
    const menuItems = Array.from(document.querySelectorAll('yt-formatted-string'));
    return menuItems.find(el => el.innerText.trim() === 'Inbox에서 삭제');
  };

  for (let i = 0; i < maxDeleteCount; i++) {
    const btn = getFirstMenuButton();
    if (!btn) break;

    // 메뉴 열기: 이벤트를 순서대로 발생
    ['pointerdown', 'mousedown', 'mouseup', 'click'].forEach(type => {
      btn.dispatchEvent(new MouseEvent(type, {
        bubbles: true, cancelable: true, view: window
      }));
    });

    await delay(500); // 메뉴가 뜰 시간 대기

    const deleteBtn = clickDeleteMenu();
    if (deleteBtn) {
      deleteBtn.click();
    } else {
      break;
    }

    await delay(1500); // 영상 목록 갱신 대기
  }
  return '반복 삭제 완료';
})();

비동기 실행과 delay 패턴

즉시 실행 함수(IIFE)를 async로 선언한다. await delay(500)처럼 적절한 대기 시간을 넣어야 DOM이 갱신된다. YouTube같은 SPA(Single Page Application)에서는 클릭 후 DOM 업데이트에 시간이 걸린다. 대기 없이 다음 동작을 실행하면 요소를 찾지 못한다.

dispatchEvent로 클릭 시뮬레이션

단순히 element.click()만으로는 동작하지 않는 경우가 있다. YouTube처럼 복잡한 이벤트 핸들러를 사용하는 사이트에서는 pointerdown, mousedown, mouseup, click을 순서대로 발생시켜야 한다. bubbles: truecancelable: true 옵션도 필수다.

활용 팁

  • maxDeleteCount 값을 변경하면 한 번에 삭제하는 영상 수를 조절할 수 있다.
  • "Inbox에서 삭제" 텍스트는 YouTube 언어 설정에 따라 달라진다. 영문이면 "Remove from"으로 수정해야 한다.
  • 이 패턴은 YouTube 외에도 반복적인 DOM 조작이 필요한 모든 웹사이트에 적용할 수 있다.

매크로 분석: Youtube - Markdown save

무엇을 하는 매크로인가

YouTube 영상 URL을 받아서 Python 스크립트로 마크다운 요약 파일을 생성한다. 트리거 없이 외부에서 URL을 전달받아 실행한다.

트리거

트리거 타입 설명
(없음) -- 외부 매크로에서 URL을 전달받아 실행

액션 흐름

flowchart TD A(["외부 매크로에서 URL 전달받아 실행"]) --> B["SetActionDelay\n액션 간 딜레이 설정"] B --> C["변수 설정: computerName\n= %MacName%"] C --> D["변수 설정: url\n= %TriggerValue%"] D --> E["SearchReplace\nURL에서 TriggerValue= 접두사 제거"] E --> F{"computerName이\nlearningstack으로\n시작하는가?"} F -- 아니오 --> G(["종료 -- 다른 Mac에서는 실행하지 않음"]) F -- 예 --> H["ExecuteShellScript 비동기 실행\npython youtube-summarize/main.py"] H --> I(["마크다운 파일 생성 완료"]) style A fill:#E3F2FD,stroke:#1976D2 style B fill:#F3E5F5,stroke:#7B1FA2 style C fill:#F3E5F5,stroke:#7B1FA2 style D fill:#F3E5F5,stroke:#7B1FA2 style E fill:#F3E5F5,stroke:#7B1FA2 style F fill:#FFF8E1,stroke:#FFA000 style G fill:#E3F2FD,stroke:#1976D2 style H fill:#E8F5E9,stroke:#388E3C style I fill:#E3F2FD,stroke:#1976D2

그림: Youtube - Markdown save 매크로의 액션 흐름. 변수로 URL을 받아 컴퓨터 이름을 확인한 뒤, 조건에 맞으면 Python 스크립트를 비동기로 실행한다.

  1. SetActionDelay -- 액션 간 딜레이를 설정한다.
  2. 변수 설정: computerName -- %MacName%으로 현재 컴퓨터 이름을 저장한다.
  3. 변수 설정: url -- %TriggerValue%로 전달받은 URL을 저장한다.
  4. SearchReplace -- URL 변수에서 TriggerValue= 접두사를 제거한다.
  5. 조건 분기 -- computerName이 "learningstack"으로 시작하는지 확인한다.
  6. 조건 충족 시 -- Python 스크립트(youtube-summarize/main.py)를 비동기로 실행한다. URL은 변수로 전달한다.

핵심 기술 해설

변수를 활용한 데이터 전달

%TriggerValue%는 매크로를 호출할 때 전달되는 값이다. 이 매크로는 URL을 인자로 받아서 url 변수에 저장한다. SearchReplace 액션으로 불필요한 접두사를 정리한다.

%TriggerValue%  -->  "TriggerValue=https://youtube.com/watch?v=xxx"
SearchReplace   -->  "https://youtube.com/watch?v=xxx"

컴퓨터 이름으로 환경 분기

%MacName% 토큰은 현재 Mac의 컴퓨터 이름을 반환한다. 여러 Mac에서 같은 매크로를 동기화할 때, 컴퓨터별로 다른 동작을 수행할 수 있다. 이 매크로에서는 "learningstack"이라는 이름의 Mac에서만 Python 스크립트를 실행한다.

비동기 셸 스크립트 실행

ExecuteShellScript의 실행 모드를 "Asynchronously"로 설정했다. Python 스크립트의 실행 시간이 길기 때문에, 매크로가 끝날 때까지 기다리지 않는다. 결과는 url 변수를 입력으로 전달받아 처리한다.

/Users/.../youtube-summarize/.venv/bin/python /Users/.../youtube-summarize/main.py

Python 가상환경(.venv)의 실행 파일을 직접 지정한다. 시스템 Python이 아닌 프로젝트 전용 환경을 사용하는 것이 안정적이다.

활용 팁

  • Python 스크립트 경로에 $HOME을 사용하면 여러 Mac에서 동일한 매크로를 공유할 수 있다.
  • 비동기 실행이므로 결과를 확인하려면 별도의 알림이나 변수 체크 로직이 필요하다.

매크로 분석: Youtube - [List] Markdown & Google Docs save

무엇을 하는 매크로인가

YouTube 재생목록 페이지에서 모든 영상 링크를 추출한 뒤, 각 영상을 Gemini에 보내 분석하고 마크다운으로 저장한다. 가장 복잡한 매크로로, JavaScript, 반복문, 셸 스크립트, 변수를 조합한다.

트리거

트리거 타입 설명
(없음) -- 수동 실행. YouTube 재생목록 페이지가 열려 있어야 한다

액션 흐름

  1. SafariControl (WaitForComplete) -- 현재 페이지 로딩이 완료될 때까지 대기한다.
  2. Pause 3초 -- 동적 콘텐츠 로딩을 위해 추가 대기한다.
  3. ExecuteJavaScript -- 페이지에서 모든 영상 링크를 추출하여 youtuble_links 변수에 저장한다.
  4. For Each 반복 -- youtuble_links 변수의 각 줄(URL)에 대해 반복한다:
    • Gemini 웹사이트를 열고 페이지 로딩을 기다린다.
    • 영상 분석 프롬프트와 URL을 입력한다.
    • Enter를 눌러 Gemini에 전송한다.
    • Python 스크립트로 마크다운 파일을 생성한다.
    • markdown_create_result 변수가 "성공" 또는 "실패"가 될 때까지 대기한다.
    • 변수를 "초기화"로 리셋하고, 0.5초 대기 후 다음 영상으로 진행한다.

핵심 기술 해설

JavaScript로 DOM에서 데이터 추출

재생목록 페이지의 모든 영상 링크를 한 번에 추출하는 JavaScript다. querySelectorAll로 링크 요소를 수집하고, URL 객체로 변환하여 중복을 제거한다.

return Array.from(document.querySelectorAll('a[href^="/watch?v="]'))
  .map(el => {
    try {
      return new URL(`https://www.youtube.com${el.getAttribute('href')}`);
    } catch (e) {
      return null;
    }
  })
  .filter(Boolean)
  .filter((url, i, self) => {
    const v = url.searchParams.get("v");
    return self.findIndex(u => u.searchParams.get("v") === v) === i;
  })
  .map(url => `https://www.youtube.com/watch?v=${url.searchParams.get("v")}`)
  .join('\n');

핵심 포인트가 세 가지 있다.

첫째, a[href^="/watch?v="] 선택자로 영상 링크만 필터링한다. YouTube 페이지에는 수많은 링크가 있지만, 영상 링크는 /watch?v=로 시작한다.

둘째, URL 객체를 사용하여 쿼리 파라미터를 정확히 파싱한다. YouTube URL에는 list, index 같은 불필요한 파라미터가 붙는다. searchParams.get("v")로 영상 ID만 추출하면 깔끔한 URL이 된다.

셋째, findIndex로 같은 영상 ID를 가진 중복 URL을 제거한다. 재생목록 페이지에서는 같은 영상이 여러 링크로 나타날 수 있다.

JavaScript 결과를 KM 변수로 전달

ExecuteJavaScript 액션에서 return 문으로 값을 반환하면, KM이 이를 변수에 저장한다. 이 매크로에서는 줄바꿈(\n)으로 구분된 URL 목록을 youtuble_links 변수에 저장한다.

For Each 반복과 PauseUntil 동기화

For Each 액션은 변수의 각 줄을 순서대로 youtuble_link에 넣고 반복한다. 반복문 안에서 Python 스크립트를 비동기로 실행하고, PauseUntil로 결과를 기다린다. markdown_create_result 변수가 "성공" 또는 "실패"가 되면 다음 반복으로 넘어간다.

이 패턴은 비동기 작업과 동기 흐름을 결합할 때 유용하다. 셸 스크립트가 오래 걸려도 KM이 다음 액션을 실행하지 않고 기다린다.

활용 팁

  • Gemini 프롬프트를 수정하면 요약 형식을 자유롭게 바꿀 수 있다. 현재는 해시태그, 요약, 타임스탬프를 포함한다.
  • Pause 3초는 YouTube 페이지의 동적 로딩 시간이다. 네트워크 속도에 따라 조절이 필요하다.
  • 이 매크로 패턴은 YouTube 외에도 "웹에서 목록 추출 -> AI 분석 -> 파일 저장" 워크플로우에 범용적으로 적용된다.

직접 만들어보기

가장 핵심적인 "재생목록에서 목록 삭제" 매크로를 직접 만들어보자. ExecuteJavaScript의 기본 사용법을 익히기에 적합하다.

  1. KM 에디터에서 새 매크로를 만들고 이름을 "Youtube - 재생목록 삭제"로 설정한다.
  2. 매크로 그룹의 Available in 설정에서 Chrome만 선택한다.
  3. Execute JavaScript in Google Chrome 액션을 추가한다.
  4. 다음 JavaScript 코드를 입력한다:
(async () => {
  const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
  const maxDeleteCount = 5; // 처음에는 5개로 테스트

  const getFirstMenuButton = () => {
    const items = document.querySelectorAll('ytd-playlist-video-renderer');
    if (!items.length) return null;
    return items[0].querySelector('button[aria-label="작업 메뉴"]');
  };

  const clickDeleteMenu = () => {
    const menuItems = Array.from(
      document.querySelectorAll('yt-formatted-string')
    );
    return menuItems.find(el => el.innerText.trim() === 'Inbox에서 삭제');
  };

  for (let i = 0; i < maxDeleteCount; i++) {
    const btn = getFirstMenuButton();
    if (!btn) break;

    ['pointerdown', 'mousedown', 'mouseup', 'click'].forEach(type => {
      btn.dispatchEvent(new MouseEvent(type, {
        bubbles: true, cancelable: true, view: window
      }));
    });

    await delay(500);
    const deleteBtn = clickDeleteMenu();
    if (deleteBtn) deleteBtn.click();
    else break;

    await delay(1500);
  }
  return '삭제 완료';
})();
  1. Chrome에서 YouTube 재생목록 페이지를 연다.
  2. 매크로를 실행하면 영상이 하나씩 삭제되는 것을 확인할 수 있다.

주의사항:

  • "작업 메뉴"와 "Inbox에서 삭제"는 YouTube 언어 설정에 따라 다르다. 자신의 YouTube 설정에 맞게 텍스트를 수정한다.
  • maxDeleteCount를 처음부터 크게 설정하지 않는다. 테스트 후 숫자를 늘린다.
  • delay 값은 네트워크 속도에 따라 조절한다. 삭제가 안 되면 값을 늘려본다.

이 챕터에서 배운 것

  • ExecuteShellScript 안에서 osascript로 활성 앱과 URL을 확인하는 패턴
  • ExecuteJavaScript로 브라우저 DOM을 직접 조작하는 방법
  • async/await와 delay를 사용한 비동기 DOM 조작 패턴
  • dispatchEvent로 복잡한 클릭 이벤트를 시뮬레이션하는 방법
  • JavaScript 실행 결과를 KM 변수에 저장하고, For Each 반복문에서 활용하는 패턴
  • PauseUntil로 비동기 스크립트의 완료를 기다리는 동기화 기법

다음 챕터 예고

ch10에서는 조건 분기와 변수를 다룬다. Slack 상태를 자동으로 변경하고, 헤드폰이나 모니터 연결 상태에 따라 다른 동작을 수행하는 매크로를 만든다. IfThenElse의 중첩, 변수 비교, 환경 조건 감지를 깊이 있게 살펴본다.