2026년 NODRIVER를 활용한 웹 스크래핑

NODRIVER로 웹 스크래핑을 마스터하세요. 이 가이드는 설정, 주요 기능, 데이터 추출 워크플로우를 간소화하는 방법을 다룹니다.
3 분 읽기
web scraping with NODRIVER blog image

수년간 Undetected Chromedriver는 안전한 브라우징과 봇 우회 방지의 핵심 도구였습니다. Undetected Chromedriver 개발자는 이후 NODRIVER를 제작했습니다. NODRIVER를 사용하면 더 이상 Selenium이나 Webdriver에 의존할 필요가 없습니다. 간단한 pip 설치만으로 모든 준비가 완료됩니다.

이 가이드에서 배우게 될 내용:

  • NODRIVER란 무엇인가요?
  • 다른 헤드리스 브라우저와 어떻게 다른가?
  • NODRIVER 사용법
  • NODRIVER의 한계는 무엇인가요?
  • 프록시와 함께 NODRIVER를 사용하는 방법?
  • NODRIVER의 확실한 대안

NODRIVER란 무엇이며 왜 관심을 가져야 할까요?

NODRIVER란 정확히 무엇인가요?

NODRIVER는 Undetected Chromedriver의 완전 비동기식 후속 버전입니다. 모든 kwargs에 대해 “모범 사례”를 기본값으로 사용하여, 이 도구는 최소한의 코드만으로 바로 사용할 수 있도록 설계되었습니다.

NODRIVER는 다음과 같은 기능을 자랑합니다:

  • 성능
  • 외부 의존성 없음 (Chromedriver조차도)
  • 안티봇 우회
  • 지속적인 세션 쿠키
  • 사용 시마다 새로 생성되는 브라우저 인스턴스

NODRIVER의 차별점은 무엇인가요?

NODRIVER는 Undetected Chromedriver 및 기타 헤드리스 브라우저와 근본적으로 다른 아키텍처를 사용합니다. 기존 브라우저들은 Selenium이나 Chrome DevTools Protocol (CDP)에 의존해 왔습니다.

NODRIVER는 자체 개발한 DevTools 프로토콜 구현체를 사용합니다. 문서에서는 이를 “크롬(과 유사한) 자동화 라이브러리”라고 명시합니다. NODRIVER를 사용하면 Selenium에 의존하지 않으며, CDP에 직접적으로 의존하지도 않습니다. NODRIVER는 CDP의 맞춤형 구현체를 사용합니다. NODRIVER를 사용하려면 pip와 크롬 기반 브라우저만 있으면 됩니다.

NODRIVER를 이용한 스크래핑

1. 시작하기

시작하기 전에 Python과 브라우저가 설치되어 있는지 확인해야 합니다. 이 글을 읽고 계시다면 이미 설치되어 있을 거라 가정합니다. pip로 NODRIVER를 직접 설치할 수 있습니다.

pip install nodriver

2. 기본 구조

기본 구조는 Playwright나 Puppeteer와 매우 유사합니다. Python에서 Playwright 사용에 관심이 있다면, 아마존 리스팅 스크래핑에 대한 전체 가이드를 여기서 확인할 수 있습니다. NODRIVER는 Playwright와 매우 비슷한 느낌을 주지만, 아직 활발한 개발 단계에 있습니다.

기본 구조는 다음과 같습니다.

import nodriver

async def main():
    # 브라우저 시작
    browser = await nodriver.start()

    base_url = "https://quotes.toscrape.com"

    # 페이지 이동
    page = await browser.get(base_url)

    ### 로직 삽입 위치 ###

    #브라우저 닫기
    await page.close()

if __name__ == '__main__':

    #문서에서 asyncio.run() 사용을 직접 권장하지 않음
    nodriver.loop().run_until_complete(main())

3. 페이지 가져오기

위 기본 뼈대에서 보셨듯이, browser.get()페이지 객체를 반환합니다. 여러 페이지를 동시에 열 수도 있습니다. 창의력을 발휘한다면 고도의 동시 작업을 구현할 수도 있습니다.

아래 코드 조각은 이론적인 예시입니다.

#페이지로 이동
page_1 = await browser.get(base_url)
page_2 = await browser.get(a_different_url)

####서로 다른 페이지로 작업 수행#####

4. 동적 콘텐츠

동적 콘텐츠를 처리하려면 두 가지 옵션이 있습니다. .sleep() 메서드를 사용해 임의의 시간 동안 대기하거나, .wait_for() 를 사용해 페이지의 특정 선택자를 기다릴 수 있습니다.

#임의 시간 대기
await tab.sleep(1)

#특정 요소 대기
await tab.wait_for("div[data-testid='some-value']")

참고: 위 코드 조각에서는 페이지 대신 탭을 변수명으로 사용했습니다. 둘은 상호 교환 가능합니다. 둘 다 탭 객체입니다. NODRIVER의 탭에 대해 자세히 알아보려면 여기를 참조하세요.

5. 요소 찾기

NODRIVER는 페이지에서 요소를 찾는 다양한 메서드를 제공합니다. 일부 레거시 메서드 처리가 진행 중인 것으로 보입니다.

요소 찾기에 사용되는 텍스트 기반 메서드는 네 가지가 있습니다. 그중 두 가지는 향후 사라질 가능성이 높습니다.

#텍스트를 이용해 요소 찾기
my_element = page.find("some text here")

# 텍스트로 요소 목록 찾기
my_elements = page.find_all("some text here")

# 텍스트로 요소 찾기
my_element = page.find_element_by_text("some text here")

# 텍스트로 요소 목록 찾기
my_elements = page.find_element_by_text("some text here")

위 메서드들과 마찬가지로, 요소를 찾는 데 사용되는 네 가지 선택기 기반 메서드도 있습니다. 그중 두 가지는 사라질 가능성이 높습니다. NODRIVER 개발자들이 CDP와 명확히 정렬하고자 한다면, query_selector 메서드들은 살아남을 가능성이 높습니다.

#CSS 선택자로 단일 요소 찾기
my_element = page.select("div[class='your-classname']")

#CSS 선택자로 요소 목록 찾기
my_elements = page.select_all("div[class='your-classname']") 

#CSS 선택자로 단일 요소 찾기
my_element = page.query_selector("div[class='your-classname']")

#CSS 선택자로 요소 목록 찾기
my_elements = page.query_selector_all("div[class='your-classname']") 

위에서 볼 수 있듯이, 페이지에서 요소를 찾는 방법에는 여러 가지가 있습니다. 시간이 지나면 NODRIVER 개발자들이 이를 개선할 수도 있겠지만, 현재로서는 그들의 파싱 방법이 스위스 군용 체인톱처럼 다용도로 활용되고 있습니다.

6. 데이터 추출

NODRIVER는 데이터를 추출하는 몇 가지 방법을 제공합니다. .attributes 트레이트를 사용해 속성을 직접 추출할 수 있지만, 이는 사용자 친화적이지 않습니다. JSON 객체가 아닌 배열을 반환하기 때문입니다.

링크 객체에서 href를 추출하기 위해 제가 만든 임시 해결책입니다. 보기 좋진 않지만 작동합니다. attributes 메서드는 곧 좀 더 기능적인 것으로 대체될 것으로 예상됩니다.

next_button = await page.select("li[class='next'] > a")

# 배열을 반환함
attributes = next_button.attributes

# 배열 인덱싱을 사용해 href 객체와 그 값 찾기
for i in range(len(attributes)):
    if attributes[i] == "href":
        next_url = attributes[i+1]

참고: 대부분의 다른 헤드리스 브라우저에는 get_attribute() 메서드가 포함되어 있습니다. 그러나 이 메서드는 아직 NODRIVER에서 작동하지 않습니다.

텍스트 데이터를 추출하는 방법은 다음과 같습니다. 보시다시피 여기서는 await를 사용하지 않습니다. 향후 다른 CDP 스타일 브라우저와 일치하도록 변경될 것으로 예상됩니다. 현재 형태에서는 text가 메서드가 아닌 단순한 속성입니다 — 속성과 함께 await를 사용하면 실제로 오류가 발생합니다. 이는 Puppeteer와 Playwright 모두와 상반되는 느낌이지만, 이는 NODRIVER의 현재 상태입니다 — 여전히 활발한 개발 중입니다.

# 인용문 요소 찾기
quote_element = await quote.query_selector("span[class='text']")
# 텍스트 추출
quote_text = quote_element.text

7. 데이터 저장

추출한 데이터를 깔끔한 JSON 파일 안에 저장하겠습니다. 인용문을 추출할 때 각 인용문에는 태그 목록이 포함되는데, 목록은 CSV 형식으로 처리하기에 적합하지 않습니다.

import json

with open("quotes.json", "w", encoding="utf-8") as f:
    json.dump(scraped_data, f, ensure_ascii=False, indent=4)

8. 모든 것 통합하기

이제 이 모든 개념을 실제 작동하는 스크립트로 통합해 보겠습니다. 아래 예시에서는 위 개념들을 활용해 튜토리얼 스크래핑 전용 사이트인 ‘Qutoes to Scrape ‘에서 데이터를 추출합니다. 아래 코드를 복사하여 붙여넣어 NODRIVER가 실제로 어떻게 작동하는지 직접 체험해 보세요.

import nodriver
import json

async def main():

    # 스크랩된 데이터를 담을 리스트
    scraped_data = []


    browser = await nodriver.start()

    next_url = "/"

    base_url = "https://quotes.toscrape.com"

    #스크랩할 URL이 남아있는 동안 반복
    while next_url:

        #페이지로 이동
        page = await browser.get(f"{base_url}{next_url}")

        #셀렉터로 인용문 div 찾기
        quotes = await page.select_all("div[class='quote']")

        #인용문 반복 처리
        for quote in quotes:

            #인용문 요소 찾고 텍스트 추출
            quote_element = await quote.query_selector("span[class='text']")
            quote_text = quote_element.text

            #저자 찾기 및 텍스트 추출
            author_element = await quote.query_selector("small[class='author']")
            author = author_element.text

            #태그 요소 찾기
            tag_elements = await quote.query_selector_all("a[class='tag']")
            tags = []

            #태그 반복 처리 및 텍스트 추출
            for tag_element in tag_elements:
                text = tag_element.text
                tags.append(text)

            #추출된 데이터를 스크랩된 데이터 목록에 추가
            scraped_data.append({
                "quote": quote_text,
                "author": author,
                "tags": tags
            })

        #페이지에 "다음" 버튼이 있는지 확인
        next_button = await page.select("li[class='next'] > a")

        #존재하지 않으면 브라우저를 닫고 루프를 종료
        if next_button == None:
            await page.close()
            next_url = None

        #존재할 경우 다음 블록 실행
        else:
            attributes = next_button.attributes

            #원하는 속성(값이 다음 인덱스)을 찾기 위해 속성 반복
            for i in range(len(attributes)):
                if attributes[i] == "href":
                    next_url = attributes[i+1]

    # 데이터를 json 파일에 기록
    with open("quotes.json", "w", encoding="utf-8") as f:
        json.dump(scraped_data, f, ensure_ascii=False, indent=4)


if __name__ == '__main__':

    nodriver.loop().run_until_complete(main())

위 스크립트를 실행하면 아래와 같은 객체가 포함된 JSON 파일을 얻을 수 있습니다.

[
    {
        "quote": "“우리가 창조한 세상은 우리의 사고 과정이다. 우리의 사고를 바꾸지 않고서는 바꿀 수 없다.”",
        "author": "Albert Einstein",
        "tags": [
            "change",
            "deep-thoughts",
            "thinking",
            "world"
        ]
    },
    {
        "quote": "“해리, 우리의 진정한 모습을 드러내는 것은 능력보다 선택이다.”",
        "author": "J.K. 롤링",
        "tags": [
            "ability",
            "choice"
        ]
    },

NODRIVER의 현재 한계점

현재 NODRIVER에는 주목할 만한 몇 가지 심각한 한계가 있습니다. 이를 살펴보겠습니다.

헤드리스 모드

NODRIVER는 헤드리스 모드에서 실행할 때마다 오류를 발생시킵니다. 이것이 의도된 것(봇 방지 우회)인지, 아니면 정당한 문제인지는 확실하지 않습니다.

A Python error traceback displayed in a terminal window, indicating a maximum recursion depth exceeded error in a Nodriver script, with file paths and line numbers referenced. The error suggests issues with preparing a headless session and sending a one-shot request.

페이지 상호작용

NODRIVER 문서에는 다양한 페이지 상호작용 기능이 기재되어 있지만, 대부분 부분적으로 작동하거나 아예 작동하지 않습니다. 아래 스크린샷에서 click_mouse()mouse_click() 모두에 대해 이 문제가 문서화되어 있음을 확인할 수 있습니다.

Code snippet showing the parameters and usage of async functions mouse_click and click_mouse, including details on button selection, modifier keys, and an internal event for waiting.

속성 추출

NODRIVER의 가장 큰 문제점은 속성 추출입니다. 앞서 언급했듯이, 이 기능은 배열을 출력하며, href 작업around에서 보셨듯이 매우 구식입니다. 다음은 attribute의 실제 출력 결과입니다. 생산 환경 수준의 스크래핑을 위해서는 이 문제가 해결되어야 합니다.

A snippet of code showing a hyperlink attribute with the value '/page/2/' in an array format.

NODRIVER에서의 프록시 사용

현재 NODRIVER의 프록시 지원은 제한적입니다. 프록시 연결을 위한 create_context() 메서드는 제공되지만,

아래 코드 조각은 공식 이슈 페이지에서 직접 가져온 것입니다. 그러나 이 방법과 다양한 다른 방법을 수시간 동안 시도해 봤음에도 연결에 실패했습니다.

tab = await browser.create_context("https://www.google.nl", proxy_server='socks5://myuser:mypass@somehost')

# 새 창을 원한다면 new_window=True 추가

문서를 살펴보면 프록시 관련 섹션[1]이 있습니다. 공식 프록시 섹션이 있음에도 실제 문서화는 되어 있지 않습니다. 가까운 시일 내에 수정될 것으로 예상됩니다.

실용적인 대안

현재는 실제 운영 환경에 바로 적용하기에는 무리가 있지만, NODRIVER의 미래 발전 가능성은 매우 높습니다. 보다 강력한 도구를 원하신다면 아래 브라우저들을 살펴보세요.

  • Selenium: 2004년부터 꾸준히 발전해 왔습니다. Selenium은 Chromedriver에 의존하지만, 검증된 성능과 생산 환경 적용이 가능합니다. Selenium 웹 스크래핑에 대해 자세히 알아보세요.
  • Playwright: Playwright는 본 튜토리얼에서 NODRIVER로 살펴본 내용을 정제하고 즉시 사용 가능한 버전으로 구현한 느낌입니다. Playwright를 활용한 웹 스크래핑 방법을 알아보세요.

결론

NODRIVER는 브라우저 자동화를 위한 흥미로운 신기술이지만, 빠른 개발 속도로 인해 일부 기능은 아직 성숙 단계에 있습니다. 대규모의 안정적인 웹 스크래핑을 위해서는 다음과 같은 견고한 솔루션을 고려해 보세요:

무료 체험판에 가입하고 지금 바로 시작하세요!