๐Ÿ“š ์•Œ๋ผ๋”˜ ์ž๋™ ๋กœ๊ทธ์ธ๊ณผ ์ฃผ๋ฌธ ๋„์„œ ์ˆ˜์ง‘ ์ž๋™ํ™”: ์‹คํŒจ ์‚ฌ๋ก€์™€ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ

@leekh8 ยท June 20, 2025 ยท 8 min read

Code N Solve

๐Ÿ“˜: ์•Œ๋ผ๋”˜ ์ž๋™ ๋กœ๊ทธ์ธ๊ณผ ๋„์„œ ์ˆ˜์ง‘ ์ž๋™ํ™” ์‹คํŒจ ์‚ฌ๋ก€ ์ •๋ฆฌ

์ตœ๊ทผ์— ๊ณผ๊ฑฐ ๊ตฌ๋งคํ•œ ๋„์„œ ๋‚ด์—ญ์„ ์ฒด๊ณ„์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ์‹ถ์–ด์„œ, ์•Œ๋ผ๋”˜์—์„œ ์ฃผ๋ฌธ ๋‚ด์—ญ์„ ์ž๋™์œผ๋กœ ์ˆ˜์ง‘ํ•˜๋Š” ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•ด๋ณด์•˜๋‹ค.

Playwright1๋ฅผ ์‚ฌ์šฉํ•œ ๋ธŒ๋ผ์šฐ์ € ์ž๋™ํ™”์™€ GitHub Actions2๋ฅผ ํ†ตํ•œ ์ฃผ๊ธฐ์  ์‹คํ–‰์œผ๋กœ ์™„์ „ ์ž๋™ํ™”๋ฅผ ๋ชฉํ‘œ๋กœ ํ–ˆ์ง€๋งŒ, ์˜ˆ์ƒ๋ณด๋‹ค ํ›จ์”ฌ ๋งŽ์€ ๋ฌธ์ œ์— ์ง๋ฉดํ–ˆ๋‹ค.

ํŠนํžˆ headless Chrome ํ™˜๊ฒฝ์—์„œ์˜ ๋กœ๊ทธ์ธ ์ฐจ๋‹จ, ์„ธ์…˜ ๊ด€๋ฆฌ ๋ณต์žก์„ฑ, Git ์ž๋™ ์ปค๋ฐ‹ ์˜ค๋ฅ˜ ๋“ฑ ๋‹ค์–‘ํ•œ ์‹คํŒจ ์‚ฌ๋ก€์™€ ํ•ด๊ฒฐ ๊ณผ์ •์„ ์ •๋ฆฌํ•ด๋ณด์•˜๋‹ค.

๐Ÿšจ 1. Playwright ์ž๋™ ๋กœ๊ทธ์ธ ์‹คํŒจ - ๋ด‡ ํƒ์ง€ ์ฐจ๋‹จ

๐Ÿšจ ๋ฌธ์ œ ์ƒํ™ฉ

โŒ ๋กœ๊ทธ์ธ ์‹คํŒจ: waiting for navigation to "**/account/wmaininfo.aspx*" until 'load'
navigated to "https://www.aladin.co.kr/login/wlogin_popup_result.aspx?SecureOpener=1"

๐Ÿง ์›์ธ ๋ถ„์„

  • ์•Œ๋ผ๋”˜์˜ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€(wlogin_popup.aspx?SecureOpener=1)๋Š” ํŒ์—… ์ „์šฉ ํŽ˜์ด์ง€๋กœ ์„ค๊ณ„๋˜์–ด ์žˆ์œผ๋ฉฐ, ์ž๋™ํ™” ๋ธŒ๋ผ์šฐ์ € ํƒ์ง€๊ฐ€ ๋งค์šฐ ๊ฐ•๋ ฅํ•˜๊ฒŒ ์ ์šฉ๋จ.

  • GitHub Actions์˜ headless Chromium ํ™˜๊ฒฝ์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํŠน์ง•๋“ค์ด ์ž๋™ํ™” ๋„๊ตฌ๋กœ ์ธ์‹๋จ:

    • navigator.webdriver ์†์„ฑ์ด true๋กœ ์„ค์ •
    • ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž์™€ ๋‹ค๋ฅธ User-Agent ํ—ค๋”
    • JavaScript ์‹คํ–‰ ํŒจํ„ด์˜ ์ฐจ์ด
    • ๋งˆ์šฐ์Šค/ํ‚ค๋ณด๋“œ ์ด๋ฒคํŠธ ํƒ€์ด๋ฐ์˜ ๋ถ€์ž์—ฐ์Šค๋Ÿฌ์›€
  • ํŠนํžˆ SecureOpener=1 ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ํฌํ•จ๋œ ํŒ์—… ํŽ˜์ด์ง€๋Š” window.opener ๊ฐ์ฒด๋ฅผ ์š”๊ตฌํ•˜๊ฑฐ๋‚˜ ํŠน์ • JavaScript ์ปจํ…์ŠคํŠธ์—์„œ๋งŒ ์ •์ƒ ์ž‘๋™ํ•˜๋„๋ก ์ œํ•œ๋˜์–ด ์žˆ์Œ.

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

1. ๋ธŒ๋ผ์šฐ์ € ํƒ์ง€ ์šฐํšŒ ์„ค์ •

# Playwright ๋ธŒ๋ผ์šฐ์ € ์„ค์ •
browser = await playwright.chromium.launch(
    headless=True,
    args=[
        "--disable-blink-features=AutomationControlled",
        "--disable-web-security",
        "--disable-features=VizDisplayCompositor"
    ]
)

# JavaScript ์†์„ฑ ์กฐ์ž‘์œผ๋กœ ํƒ์ง€ ์šฐํšŒ
context.add_init_script("""
    Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
    Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3]});
    Object.defineProperty(navigator, 'languages', {get: () => ['ko-KR', 'ko']});
    window.chrome = { runtime: {} };
""")

2. ์ž์—ฐ์Šค๋Ÿฌ์šด ์ž…๋ ฅ ์‹œ๋ฎฌ๋ ˆ์ด์…˜

# ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž์™€ ์œ ์‚ฌํ•œ ์ž…๋ ฅ ํŒจํ„ด ์ ์šฉ
await page.type("#Email", ALADIN_ID, delay=100)  # 100ms ๊ฐ„๊ฒฉ์œผ๋กœ ํƒ€์ดํ•‘
await page.type("#Password", ALADIN_PW, delay=150)
await page.wait_for_timeout(1000)  # ์ž…๋ ฅ ํ›„ 1์ดˆ ๋Œ€๊ธฐ

# ๋‹ค์–‘ํ•œ ๋กœ๊ทธ์ธ ์‹œ๋„ ๋ฐฉ์‹ ์ ์šฉ
try:
    await page.click("input[type='submit'][value='๋กœ๊ทธ์ธ']")
except:
    await page.eval_on_selector("#LoginForm", "form => form.submit()")

3. ๋กœ๊ทธ์ธ ์„ฑ๊ณต ํŒ๋‹จ ๋กœ์ง ๊ฐœ์„ 

# URL ๋ณ€ํ™” ๋Œ€์‹  DOM ์š”์†Œ๋กœ ์„ฑ๊ณต ์—ฌ๋ถ€ ํŒ๋‹จ
await page.wait_for_timeout(3000)
if page.query_selector("#Email") and await page.is_visible("#Email"):
    print("โŒ ๋กœ๊ทธ์ธ ์‹คํŒจ: ๋กœ๊ทธ์ธ ํผ์ด ์—ฌ์ „ํžˆ ํ‘œ์‹œ๋จ")
    return False
else:
    print("โœ… ๋กœ๊ทธ์ธ ์„ฑ๊ณต์œผ๋กœ ์ถ”์ •๋จ")
    return True

๐Ÿšจ 2. ์„ธ์…˜ ํŒŒ์ผ ์—†์Œ - GitHub Actions ํ™˜๊ฒฝ ๋ฌธ์ œ

โŒ ๋ฌธ์ œ ์ƒํ™ฉ

FileNotFoundError: [Errno 2] No such file or directory: 'storage/aladin_storage.json'

๐Ÿง ์›์ธ ๋ถ„์„

  • GitHub Actions์—์„œ save_aladin_session.py๊ฐ€ ๋กœ๊ทธ์ธ ์‹คํŒจ๋กœ ์ธํ•ด ์„ธ์…˜ ํŒŒ์ผ(aladin_storage.json) ์ƒ์„ฑ์— ์‹คํŒจ.
    • GitHub Actions ํ™˜๊ฒฝ์—์„œ Playwright๊ฐ€ ๋กœ๊ทธ์ธ์— ์‹คํŒจํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋ฉฐ, ์ด๋Š” ์„ธ์…˜ ์ €์žฅ ์ด์ „ ๋‹จ๊ณ„์—์„œ ์™„์ „ํžˆ ์ค‘๋‹จ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์ดํ›„ fetch ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๋ฌด์˜๋ฏธํ•ด์ง.
  • ์ดํ›„ fetch_aladin.py ์‹คํ–‰ ์‹œ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์„ธ์…˜ ํŒŒ์ผ์„ ์ฐธ์กฐํ•˜๋ ค๊ณ  ํ•ด์„œ FileNotFoundError ๋ฐœ์ƒ.
  • Playwright์˜ storage_state ์˜ต์…˜์€ ๋ฐ˜๋“œ์‹œ ์œ ํšจํ•œ ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ์š”๊ตฌํ•˜๋ฏ€๋กœ, ํŒŒ์ผ์ด ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด ๋ธŒ๋ผ์šฐ์ € ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ ์ž์ฒด๊ฐ€ ์‹คํŒจํ•จ.

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

1. ์„ธ์…˜ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ํ•จ์ˆ˜ ๊ตฌํ˜„

def verify_session():
    """์ €์žฅ๋œ ์„ธ์…˜์ด ์œ ํšจํ•œ์ง€ ํ™•์ธ"""
    if not os.path.exists("storage/aladin_storage.json"):
        print("โŒ ์„ธ์…˜ ํŒŒ์ผ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค")
        return False

    try:
        with open("storage/aladin_storage.json", "r") as f:
            session_data = json.load(f)

        # ์„ธ์…˜ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ํ™•์ธ
        if not session_data.get("cookies") or not session_data.get("origins"):
            print("โŒ ์„ธ์…˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค")
            return False

        print("โœ… ๊ธฐ์กด ์„ธ์…˜์ด ์œ ํšจํ•ฉ๋‹ˆ๋‹ค")
        return True
    except Exception as e:
        print(f"โŒ ์„ธ์…˜ ํ™•์ธ ์ค‘ ์˜ค๋ฅ˜: {e}")
        return False

# fetch_aladin.py ๋‚ด๋ถ€
if not STORAGE_FILE.exists():
    print("โŒ ์„ธ์…˜ ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค. ๋จผ์ € ๋กœ๊ทธ์ธ์„ ์‹คํ–‰ํ•˜์„ธ์š”.")
    return
  • fetch ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์„ธ์…˜ ํŒŒ์ผ์ด ์—†์œผ๋ฉด ์ฆ‰์‹œ ์ข…๋ฃŒ๋˜๋„๋ก ์•ˆ์ „์žฅ์น˜ ์ถ”๊ฐ€.

2. GitHub Actions ์บ์‹œ ์ „๋žต

- name: Restore Aladin session from cache
  id: restore-session
  uses: actions/cache@v4
  with:
    path: storage/aladin_storage.json
    key: aladin-session-v2-${{ secrets.ALADIN_ID }}-${{ github.run_number }}
    restore-keys: |
      aladin-session-v2-${{ secrets.ALADIN_ID }}-

- name: Generate or verify Aladin session
  env:
    ALADIN_ID: ${{ secrets.ALADIN_ID }}
    ALADIN_PW: ${{ secrets.ALADIN_PW }}
  run: |
    echo "๐Ÿ” ์„ธ์…˜ ์ƒํƒœ ํ™•์ธ ๋ฐ ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ..."
    python scripts/save_aladin_session.py

- name: Save session to cache
  if: success()
  uses: actions/cache/save@v4
  with:
    path: storage/aladin_storage.json
    key: aladin-session-v2-${{ secrets.ALADIN_ID }}-${{ github.run_number }}

3. ์กฐ๊ฑด๋ถ€ ์„ธ์…˜ ์ฒ˜๋ฆฌ ๋กœ์ง

# save_aladin_session.py
if verify_session():
    print("โœ… ๊ธฐ์กด ์„ธ์…˜์ด ์œ ํšจํ•ฉ๋‹ˆ๋‹ค")
    sys.exit(0)
else:
    print("๐Ÿ”„ ์ƒˆ ์„ธ์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค")
    if save_aladin_session():
        print("โœ… ์„ธ์…˜ ์ €์žฅ ์™„๋ฃŒ")
    else:
        print("โŒ ์„ธ์…˜ ์ƒ์„ฑ ์‹คํŒจ")
        sys.exit(1)

๐Ÿšจ 3. Git ๋ธŒ๋žœ์น˜ ์ถฉ๋Œ - origin/main vs origin/master

โŒ ๋ฌธ์ œ ์ƒํ™ฉ

fatal: invalid upstream 'origin/main'
error: cannot rebase: You have unstaged changes.
error: Please commit or stash them.
Error: Process completed with exit code 1.

๐Ÿง ์›์ธ ๋ถ„์„

  • ์ €์žฅ์†Œ์˜ ๊ธฐ๋ณธ ๋ธŒ๋žœ์น˜๊ฐ€ master์ธ๋ฐ๋„ ์›Œํฌํ”Œ๋กœ์šฐ์—์„œ origin/main์œผ๋กœ ๋ฆฌ๋ฒ ์ด์Šค๋ฅผ ์‹œ๋„ํ•จ.
  • git rebase ์‹คํ–‰ ์ „์— ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์ œ๋Œ€๋กœ ์Šคํ…Œ์ด์ง•๋˜์ง€ ์•Š์•„ "unstaged changes" ์˜ค๋ฅ˜ ๋ฐœ์ƒ.
  • Git ๋ช…๋ น์–ด ์‹คํ–‰ ์ˆœ์„œ์™€ ๋ธŒ๋žœ์น˜ ๊ฐ์ง€ ๋กœ์ง์— ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์Œ.

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

1. ๋ธŒ๋žœ์น˜ ์ž๋™ ๊ฐ์ง€ ๋กœ์ง

- name: Detect default branch
  id: branch
  run: |
    if git ls-remote --heads origin main | grep -q main; then
      echo "default_branch=main" >> $GITHUB_OUTPUT
    else
      echo "default_branch=master" >> $GITHUB_OUTPUT
    fi
    echo "ํ˜„์žฌ ๋ธŒ๋žœ์น˜: $(git branch --show-current)"
    echo "๊ธฐ๋ณธ ๋ธŒ๋žœ์น˜: $(cat $GITHUB_OUTPUT | grep default_branch)"

2. ์•ˆ์ „ํ•œ ์ปค๋ฐ‹ ๋ฐ ํ‘ธ์‹œ ๋กœ์ง

- name: Commit and push changes
  if: steps.changes.outputs.changed == 'true'
  run: |
    git config --global user.name 'github-actions[bot]'
    git config --global user.email 'github-actions[bot]@users.noreply.github.com'

    # ๋ณ€๊ฒฝ์‚ฌํ•ญ ํ™•์ธ ๋ฐ ์Šคํ…Œ์ด์ง•
    if ! git diff --quiet data/; then
      echo "๐Ÿ“ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ํ™•์ธ๋จ, ์ปค๋ฐ‹ ์ง„ํ–‰..."

      git status
      git add data/*.json

      # ์Šคํ…Œ์ด์ง•๋œ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์žˆ์„ ๋•Œ๋งŒ ์ปค๋ฐ‹
      if ! git diff --cached --quiet; then
        git commit -m "chore: update aladin book list $(date '+%Y-%m-%d %H:%M')"

        # ์•ˆ์ „ํ•œ ํ‘ธ์‹œ ์žฌ์‹œ๋„ ๋กœ์ง
        for i in {1..5}; do
          echo "๐Ÿš€ ํ‘ธ์‹œ ์‹œ๋„ ${i}/5..."

          git fetch origin ${{ steps.branch.outputs.default_branch }}

          if git rebase origin/${{ steps.branch.outputs.default_branch }}; then
            if git push origin HEAD:${{ steps.branch.outputs.default_branch }}; then
              echo "โœ… ํ‘ธ์‹œ ์„ฑ๊ณต!"
              break
            fi
          fi

          if [ $i -lt 5 ]; then
            echo "โณ 10์ดˆ ๋Œ€๊ธฐ ํ›„ ์žฌ์‹œ๋„..."
            sleep 10
          fi
        done
      fi
    fi

๐Ÿšจ 4. ๋„์„œ ์ˆ˜์ง‘ ์Šคํฌ๋ฆฝํŠธ ์‹คํŒจ - DOM ๊ตฌ์กฐ ๋ณ€ํ™” ๋Œ€์‘

โŒ ๋ฌธ์ œ ์ƒํ™ฉ

โŒ ์ฃผ๋ฌธ ๋‚ด์—ญ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค
โŒ ๋„์„œ ์ •๋ณด ์ถ”์ถœ ์‹คํŒจ

๐Ÿง ์›์ธ ๋ถ„์„

  • ์•Œ๋ผ๋”˜ ์›น์‚ฌ์ดํŠธ์˜ DOM ๊ตฌ์กฐ๊ฐ€ ์ˆ˜์‹œ๋กœ ๋ณ€๊ฒฝ๋˜์–ด ๊ณ ์ •๋œ CSS ์…€๋ ‰ํ„ฐ๋กœ๋Š” ์š”์†Œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ.
  • ํŽ˜์ด์ง€ ๋กœ๋”ฉ ํƒ€์ด๋ฐ์— ๋”ฐ๋ผ JavaScript๋กœ ๋™์  ์ƒ์„ฑ๋˜๋Š” ์š”์†Œ๋“ค์ด ๋Šฆ๊ฒŒ ๋กœ๋“œ๋จ.
  • ๋กœ๊ทธ์ธ ์ƒํƒœ๊ฐ€ ์œ ์ง€๋˜์ง€ ์•Š์•„ ์ฃผ๋ฌธ ๋‚ด์—ญ ํŽ˜์ด์ง€์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ ๋ฐœ์ƒ.

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

1. ๋‹ค์ค‘ ์…€๋ ‰ํ„ฐ ๋ฐ Fallback ์ „๋žต

def extract_books_from_page(page):
    """์—ฌ๋Ÿฌ ์…€๋ ‰ํ„ฐ๋ฅผ ์‹œ๋„ํ•˜์—ฌ ์ฃผ๋ฌธ ๋‚ด์—ญ ์ถ”์ถœ"""
    selectors = [
        "tbody#tblOrdersItem > tr",  # ๊ธฐ๋ณธ ์…€๋ ‰ํ„ฐ
        ".order-info tr",            # ๋Œ€์ฒด ์…€๋ ‰ํ„ฐ 1
        "table tr",                  # ๋Œ€์ฒด ์…€๋ ‰ํ„ฐ 2
        ".order-list-item"           # ๋Œ€์ฒด ์…€๋ ‰ํ„ฐ 3
    ]

    for selector in selectors:
        elements = page.query_selector_all(selector)
        if elements:
            print(f"โœ… ์š”์†Œ ๋ฐœ๊ฒฌ: {selector} ({len(elements)}๊ฐœ)")
            return elements

    print("โŒ ๋ชจ๋“  ์…€๋ ‰ํ„ฐ๋กœ ์š”์†Œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")
    return []

2. ๋กœ๊ทธ์ธ ์ƒํƒœ ํ™•์ธ ํ•จ์ˆ˜

def check_login_status(page):
    """ํ˜„์žฌ ํŽ˜์ด์ง€์—์„œ ๋กœ๊ทธ์ธ ์ƒํƒœ ํ™•์ธ"""
    # ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•œ ํŽ˜์ด์ง€์˜ ํŠน์ง•์  ์š”์†Œ ํ™•์ธ
    login_indicators = [
        "#Email",           # ๋กœ๊ทธ์ธ ํผ
        ".login-form",      # ๋กœ๊ทธ์ธ ํด๋ž˜์Šค
        "input[name='Email']"  # ์ด๋ฉ”์ผ ์ž…๋ ฅ
    ]

    for indicator in login_indicators:
        if page.query_selector(indicator):
            return False  # ๋กœ๊ทธ์ธ ํผ์ด ๋ณด์ด๋ฉด ๋ฏธ๋กœ๊ทธ์ธ ์ƒํƒœ

    return True  # ๋กœ๊ทธ์ธ ํผ์ด ์—†์œผ๋ฉด ๋กœ๊ทธ์ธ ์ƒํƒœ๋กœ ์ถ”์ •

3. ๋””๋ฒ„๊ทธ ์ •๋ณด ์ˆ˜์ง‘

def safe_goto(page, url, timeout=30000):
    """์•ˆ์ „ํ•œ ํŽ˜์ด์ง€ ์ด๋™ ๋ฐ ๋””๋ฒ„๊ทธ ์ •๋ณด ์ˆ˜์ง‘"""
    try:
        page.goto(url, wait_until="networkidle", timeout=timeout)
        print(f"โœ… ํŽ˜์ด์ง€ ๋กœ๋“œ ์„ฑ๊ณต: {url}")
        return True
    except Exception as e:
        print(f"โŒ ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹คํŒจ: {url}")
        print(f"์˜ค๋ฅ˜: {e}")

        # ๋””๋ฒ„๊ทธ์šฉ ์Šคํฌ๋ฆฐ์ƒท ์ €์žฅ
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        screenshot_path = f"debug_error_{timestamp}.png"
        page.screenshot(path=screenshot_path)
        print(f"๐Ÿ“ธ ์Šคํฌ๋ฆฐ์ƒท ์ €์žฅ: {screenshot_path}")

        return False
  • ์‹คํŒจ ์‹œ ๋””๋ฒ„๊น… ์ด๋ฏธ๋‹ˆ ์ €์žฅ ๋ฐ ์—…๋กœ๋“œํ•˜์—ฌ ํ™•์ธ ๊ฐ€๋Šฅํ•˜๋„๋ก ๊ตฌ์„ฑ.

๐Ÿšจ 5. GitHub Actions ์‹คํ–‰ ์‹œ๊ฐ„ ์ดˆ๊ณผ

โŒ ๋ฌธ์ œ ์ƒํ™ฉ

Error: The operation was canceled.

๐Ÿง ์›์ธ ๋ถ„์„

  • Playwright์˜ ๋ธŒ๋ผ์šฐ์ € ์‹คํ–‰๊ณผ ํŽ˜์ด์ง€ ๋กœ๋”ฉ์ด ์˜ˆ์ƒ๋ณด๋‹ค ์˜ค๋ž˜ ๊ฑธ๋ฆผ.
  • ํŠนํžˆ headless ํ™˜๊ฒฝ์—์„œ JavaScript ์‹คํ–‰์ด ๋А๋ ค์ง€๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Œ.
  • ๋„คํŠธ์›Œํฌ ์ง€์—ฐ์ด๋‚˜ ์•Œ๋ผ๋”˜ ์„œ๋ฒ„ ์‘๋‹ต ์ง€์—ฐ ์‹œ timeout ๋ฐœ์ƒ.

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

1. ์ ์ ˆํ•œ timeout ์„ค์ • ๋ฐ ์žฌ์‹œ๋„ ๋กœ์ง

jobs:
  fetch-books:
    runs-on: ubuntu-latest
    timeout-minutes: 30 # ์ถฉ๋ถ„ํ•œ ์‹คํ–‰ ์‹œ๊ฐ„ ํ™•๋ณด

    steps:
      - name: Run fetch script with retry
        run: |
          echo "๐Ÿ“š ์ฑ… ์ •๋ณด ์ˆ˜์ง‘ ์‹œ์ž‘..."
          python scripts/fetch_aladin.py || {
            echo "์ฒซ ๋ฒˆ์งธ ์‹œ๋„ ์‹คํŒจ, 30์ดˆ ํ›„ ์žฌ์‹œ๋„..."
            sleep 30
            python scripts/fetch_aladin.py
          }

2. Playwright timeout ์ตœ์ ํ™”

# ๋ธŒ๋ผ์šฐ์ € ๋ฐ ํŽ˜์ด์ง€ timeout ์„ค์ •
browser = await playwright.chromium.launch(headless=True)
context = await browser.new_context(
    storage_state="storage/aladin_storage.json" if os.path.exists("storage/aladin_storage.json") else None
)

# ํŽ˜์ด์ง€๋ณ„ timeout ์„ค์ •
page = await context.new_page()
page.set_default_timeout(60000)  # 60์ดˆ
page.set_default_navigation_timeout(60000)  # 60์ดˆ

๊ฒฐ๋ก 

  1. ๋ธŒ๋ผ์šฐ์ € ์ž๋™ํ™” ํƒ์ง€ ์šฐํšŒ: navigator.webdriver ์ œ๊ฑฐ, ์ž์—ฐ์Šค๋Ÿฌ์šด ์ž…๋ ฅ ํŒจํ„ด, User-Agent ์กฐ์ž‘์œผ๋กœ ๋ด‡ ํƒ์ง€ ํšŒํ”ผ
  2. ์„ธ์…˜ ๊ด€๋ฆฌ ์ฒด๊ณ„ํ™”: GitHub Actions ์บ์‹œ๋ฅผ ํ™œ์šฉํ•œ ์„ธ์…˜ ์ €์žฅ/๋ณต์› ๋ฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋กœ์ง ๊ตฌํ˜„
  3. Git ์ž๋™ํ™” ์•ˆ์ •ํ™”: ๋ธŒ๋žœ์น˜ ์ž๋™ ๊ฐ์ง€, ๋ณ€๊ฒฝ์‚ฌํ•ญ ํ™•์ธ ํ›„ ์ปค๋ฐ‹, ํ‘ธ์‹œ ์žฌ์‹œ๋„ ๋กœ์ง์œผ๋กœ ์•ˆ์ •์„ฑ ํ™•๋ณด
  4. DOM ๋ณ€ํ™” ๋Œ€์‘: ๋‹ค์ค‘ ์…€๋ ‰ํ„ฐ, Fallback ์ „๋žต, ๋กœ๊ทธ์ธ ์ƒํƒœ ํ™•์ธ์œผ๋กœ ์›น์‚ฌ์ดํŠธ ๋ณ€ํ™”์— ๋Œ€์‘
  5. ๋””๋ฒ„๊ทธ ์ •๋ณด ์ˆ˜์ง‘: ์‹คํŒจ ์‹œ ์Šคํฌ๋ฆฐ์ƒท ์ €์žฅ, ์ƒ์„ธ ๋กœ๊ทธ ์ถœ๋ ฅ์œผ๋กœ ๋ฌธ์ œ ์ง„๋‹จ ์šฉ์ด์„ฑ ํ™•๋ณด
  6. ์ ์ ˆํ•œ timeout ์„ค์ •: GitHub Actions์™€ Playwright ๋ชจ๋‘์—์„œ ์ถฉ๋ถ„ํ•œ ์‹คํ–‰ ์‹œ๊ฐ„ ํ™•๋ณด

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


@leekh8
Hello :)