[실무 강의] **2026년 Playwright 실전: 동적 웹페이지 데이터 10분 크롤링** 완벽 마스터 튜토리얼

학습 목표: 왜 2026년에도 Playwright인가?

2026년 현재, 웹 생태계는 더욱 복잡해진 서버 사이드 렌더링(SSR)과 고도화된 클라이언트 사이드 렌더링(CSR) 기술이 혼재되어 있습니다. 과거의 BeautifulSoup이나 단순한 Selenium만으로는 자바스크립트가 실행된 후 나타나는 동적 데이터를 수집하는 데 한계가 명확합니다. 이번 강의의 목표는 가장 강력한 브라우저 자동화 도구인 Playwright를 활용하여, 단 10분 만에 복잡한 동적 웹페이지의 데이터를 안정적으로 추출하는 실무 프로세스를 습득하는 것입니다. 단순한 데이터 수집을 넘어, 차단 방지 기술과 비동기 처리 기법을 포함한 실전 코드를 완성하게 됩니다.

사전 준비 사항

본 실습을 원활하게 진행하기 위해 아래의 개발 환경을 갖추어 주시기 바랍니다. 2026년 표준 개발 환경을 기준으로 설정되었습니다.

  • 운영체제(OS): Windows 11, macOS Sequoia 이상, 또는 Linux (Ubuntu 24.04 LTS 권장)
  • 편집기: Visual Studio Code (VS Code) 최신 버전
  • 언어 환경: Python 3.11 버전 이상 (비동기 처리를 위해 필수)
  • 필수 라이브러리 설치: 아래 명령어를 터미널에 입력하여 필요한 패키지를 설치합니다.
# 라이브러리 설치
pip install playwright
pip install pandas

# 브라우저 바이너리 설치 (Chromium, Firefox, WebKit)
playwright install

단계별 실습 과정

1단계: 비동기 Playwright 기본 구조 설계

Playwright는 기본적으로 비동기(Async) 처리를 지원합니다. 이는 대량의 데이터를 수집할 때 속도를 극대화할 수 있는 핵심 요소입니다. 먼저 브라우저를 실행하고 페이지를 여는 기본 골격 코드를 작성해 보겠습니다.

import asyncio
from playwright.async_api import async_playwright

async def run_crawler():
    async with async_playwright() as p:
        # 브라우저 실행 (headless=False로 설정하면 브라우저가 뜨는 것을 볼 수 있습니다)
        browser = await p.chromium.launch(headless=False)
        context = await browser.new_context(
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
        )
        page = await context.new_page()
        
        # 대상 URL 접속
        await page.goto("https://example.com/dynamic-data")
        
        # 페이지 로딩 대기
        await page.wait_for_load_state("networkidle")
        
        print("페이지 접속 성공!")
        await browser.close()

asyncio.run(run_crawler())

2단계: 동적 요소 핸들링과 무한 스크롤 처리

최근의 웹페이지는 스크롤을 내려야 데이터가 로드되는 ‘무한 스크롤’ 방식이 일반적입니다. Playwright의 evaluate 함수를 사용하여 자바스크립트를 직접 실행하거나, 키보드 이벤트를 통해 하단까지 스크롤하는 로직을 추가합니다.

# 스크롤 다운 함수 예시
async def scroll_to_bottom(page):
    prev_height = -1
    while True:
        current_height = await page.evaluate("document.body.scrollHeight")
        if prev_height == current_height:
            break
        prev_height = current_height
        await page.mouse.wheel(0, 1000)
        await asyncio.sleep(1.5)  # 데이터 로딩 대기 시간

3단계: 데이터 추출 및 CSS 선택자 활용

Playwright의 locator API는 매우 강력합니다. 텍스트, CSS 선택자, XPath를 자유롭게 혼용할 수 있으며, 요소가 나타날 때까지 자동으로 대기(Auto-waiting)하는 기능이 내장되어 있습니다. 상품명과 가격 데이터를 추출하는 실전 코드를 작성합니다.

async def extract_data(page):
    # 특정 요소들이 나타날 때까지 대기
    await page.wait_for_selector(".product-card")
    
    products = await page.locator(".product-card").all()
    data_list = []

    for product in products:
        name = await product.locator(".title").inner_text()
        price = await product.locator(".price-value").inner_text()
        link = await product.locator("a").get_attribute("href")
        
        data_list.append({
            "name": name.strip(),
            "price": price.replace(",", "").strip(),
            "link": f"https://example.com{link}"
        })
    
    return data_list

4단계: 데이터 저장 및 예외 처리

수집된 데이터를 CSV 파일로 저장하여 분석 가능한 형태로 만듭니다. 이 과정에서 발생할 수 있는 타임아웃이나 차단 이슈를 방지하기 위해 try-except 구문을 적용하는 것이 실무의 핵심입니다.

import pandas as pd

async def main():
    async with async_playwright() as p:
        try:
            browser = await p.chromium.launch(headless=True)
            page = await browser.new_page()
            await page.goto("https://example.com/products", timeout=60000)
            
            # 동적 로딩 처리
            await scroll_to_bottom(page)
            
            # 데이터 추출
            results = await extract_data(page)
            
            # Pandas를 이용한 저장
            df = pd.DataFrame(results)
            df.to_csv("crawling_results_2026.csv", index=False, encoding="utf-8-sig")
            print(f"총 {len(results)}개의 데이터를 성공적으로 저장했습니다.")
            
        except Exception as e:
            print(f"오류 발생: {e}")
        finally:
            await browser.close()

asyncio.run(main())

결과 확인

코드를 실행하면 터미널에 수집된 데이터의 개수가 출력되며, 프로젝트 폴더 내에 crawling_results_2026.csv 파일이 생성됩니다. 엑셀이나 VS Code의 CSV 뷰어로 확인해 보면, 자바스크립트로 생성되었던 동적 데이터들이 완벽하게 텍스트 형태로 정제되어 있는 것을 볼 수 있습니다.

마치며: 2026년 크롤링의 핵심 팁

오늘 배운 Playwright 실무 기법에서 가장 중요한 것은 ‘기다림의 미학’입니다. wait_for_selectorwait_for_load_state를 적재적소에 배치하여 브라우저와 서버의 통신 속도 차이를 극복하는 것이 안정적인 크롤러의 핵심입니다. 또한, 2026년의 강화된 봇 탐지 시스템을 피하기 위해 user_agent 설정과 적절한 sleep 타임을 주는 것을 잊지 마십시오. 이제 여러분은 어떤 복잡한 동적 웹사이트라도 자유자재로 요리할 수 있는 기술적 토대를 마련했습니다.

댓글 남기기


Warning: getimagesize(): https:// wrapper is disabled in the server configuration by allow_url_fopen=0 in /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/media.cls.php on line 1158

Warning: getimagesize(https://velog.velcdn.com/images%2Fkmw0647%2Fpost%2F986a0e76-300e-4b0c-bbff-e1ee2fa49dbc%2F%EA%B5%90%EB%B3%B4%20%ED%81%AC%EB%A1%A4%EB%A7%81%20%EA%B2%B0%EA%B3%BC.PNG): Failed to open stream: no suitable wrapper could be found in /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/media.cls.php on line 1158

Warning: getimagesize(): https:// wrapper is disabled in the server configuration by allow_url_fopen=0 in /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/media.cls.php on line 1158

Warning: getimagesize(https://velog.velcdn.com/images/hun-ing/post/04f5206c-efb6-471b-bdb1-c36f88868f74/image.png): Failed to open stream: no suitable wrapper could be found in /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/media.cls.php on line 1158

Warning: getimagesize(): https:// wrapper is disabled in the server configuration by allow_url_fopen=0 in /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/media.cls.php on line 1158

Warning: getimagesize(https://velog.velcdn.com/images%2Fkmw0647%2Fpost%2F046cff7d-f2ac-48ec-9193-b92c6d41d3ef%2F%EA%B5%90%EB%B3%B4%EB%AC%B8%EA%B3%A0%20DOM.PNG): Failed to open stream: no suitable wrapper could be found in /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/media.cls.php on line 1158

Fatal error: Uncaught ErrorException: file_put_contents(): Write of 1764 bytes failed with errno=122 Disk quota exceeded in /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/file.cls.php:177 Stack trace: #0 [internal function]: litespeed_exception_handler() #1 /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/file.cls.php(177): file_put_contents() #2 /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/optimizer.cls.php(135): LiteSpeed\File::save() #3 /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/optimize.cls.php(845): LiteSpeed\Optimizer->serve() #4 /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/optimize.cls.php(392): LiteSpeed\Optimize->_build_hash_url() #5 /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/optimize.cls.php(265): LiteSpeed\Optimize->_optimize() #6 /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/optimize.cls.php(226): LiteSpeed\Optimize->_finalize() #7 /hosting/apdldk/html/wp-includes/class-wp-hook.php(341): LiteSpeed\Optimize->finalize() #8 /hosting/apdldk/html/wp-includes/plugin.php(205): WP_Hook->apply_filters() #9 /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/core.cls.php(464): apply_filters() #10 [internal function]: LiteSpeed\Core->send_headers_force() #11 /hosting/apdldk/html/wp-includes/functions.php(5481): ob_end_flush() #12 /hosting/apdldk/html/wp-includes/class-wp-hook.php(341): wp_ob_end_flush_all() #13 /hosting/apdldk/html/wp-includes/class-wp-hook.php(365): WP_Hook->apply_filters() #14 /hosting/apdldk/html/wp-includes/plugin.php(522): WP_Hook->do_action() #15 /hosting/apdldk/html/wp-includes/load.php(1308): do_action() #16 [internal function]: shutdown_action_hook() #17 {main} thrown in /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/file.cls.php on line 177