๐Ÿ“Ž ์ฒ˜์Œ์—” ๊ทธ๋ƒฅ ๊ธ์œผ๋ฉด ๋  ์ค„ ์•Œ์•˜์Šต๋‹ˆ๋‹ค (Open Graph ํฌ๋กค๋ง ๊ฐœ๋ฐœ๊ธฐ)

LinkDropperยท2025๋…„ 4์›” 23์ผ

Link Dropper

๋ชฉ๋ก ๋ณด๊ธฐ
3/17
post-thumbnail

๋งํฌ ์ €์žฅ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค๋‹ค ๋ณด๋ฉด ๋ฐ˜๋“œ์‹œ ๋งˆ์ฃผ์น˜๋Š” ์ด์Šˆ๊ฐ€ ํ•˜๋‚˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๋ฐ”๋กœ Open Graph(OG) ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ๋น ๋ฅด๊ณ  ์ •ํ™•ํ•˜๊ฒŒ ๋ถˆ๋Ÿฌ์˜ฌ ๊ฒƒ์ธ๊ฐ€์ž…๋‹ˆ๋‹ค.

๋งํฌ๋ฅผ ์ €์žฅํ–ˆ์„ ๋•Œ, ์ธ๋„ค์ผยท์ œ๋ชฉยท์„ค๋ช…์ด ์ž๋™์œผ๋กœ ๋œจ๋Š” ๊ทธ ๊ฒฝํ—˜์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด
์šฐ๋ฆฌ๋Š” ์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ OG ๋ฐ์ดํ„ฐ๋ฅผ ํฌ๋กค๋งํ•˜๊ณ , ์–ด๋–ค ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ํ–ˆ์„๊นŒ์š”?

์˜ค๋Š˜์€ ๊ทธ ๊ฐœ๋ฐœ๊ธฐ ๊ณผ์ •์„ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค.


โ“ ์™œ open-graph-scraper๋ฅผ ์“ฐ์ง€ ์•Š์•˜๋‚˜์š”?

์ฒ˜์Œ์—๋Š” open-graph-scraper ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ• ๊นŒ ํ–ˆ์ง€๋งŒ, ์‹ค์ œ๋กœ ์ ์šฉํ•ด๋ณด๋‹ˆ ๋‘ ๊ฐ€์ง€ ์ด์œ ๋กœ ํฌ๊ธฐํ–ˆ์Šต๋‹ˆ๋‹ค.

์ฒซ์งธ, ์†๋„์ž…๋‹ˆ๋‹ค. ํ‰๊ท  2~3์ดˆ ์ •๋„๋กœ ์‘๋‹ต ์†๋„๊ฐ€ ๋А๋ ค ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์— ์˜ํ–ฅ์„ ์คฌ์Šต๋‹ˆ๋‹ค.
์ง์ ‘ ๊ตฌํ˜„ํ•œ FastAPI ๊ธฐ๋ฐ˜ ์ •์  ํฌ๋กค๋Ÿฌ๋Š” 0.2 ~ 0.4์ดˆ๋กœ ์•ฝ 10๋ฐฐ ๋นจ๋ž์Šต๋‹ˆ๋‹ค.

๋‘˜์งธ, ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ•˜๋Š” ์‚ฌ์ดํŠธ๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์•˜์Šต๋‹ˆ๋‹ค.
JavaScript๋กœ OG ํƒœ๊ทธ๋ฅผ ๋ Œ๋”๋งํ•˜๊ฑฐ๋‚˜, SPA ๊ตฌ์กฐ๋กœ ๋œ ํŽ˜์ด์ง€๋Š” ์ •๋ณด๋ฅผ ์ œ๋Œ€๋กœ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์ฃ .

์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋กœ ์ธํ•ด ์†๋„์™€ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ๋ชจ๋‘ ๋งŒ์กฑ์‹œํ‚ค๊ธฐ ์œ„ํ•ด ์ง์ ‘ ํฌ๋กค๋Ÿฌ๋ฅผ ์„ค๊ณ„ํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.


1. ๐Ÿš€ ์ฒ˜์Œ์—” ์ •์  ํฌ๋กค๋ง๋ถ€ํ„ฐ

์ดˆ๊ธฐ์—๋Š” FastAPI ๊ธฐ๋ฐ˜์˜ ์ •์  ํฌ๋กค๋Ÿฌ๋ฅผ ์ง์ ‘ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

์ง์ ‘ ๊ตฌํ˜„ํ–ˆ์„ ๋•Œ ๊ฒฐ๊ณผ๋Š”?

  • ํ‰๊ท  ์‘๋‹ต ์†๋„: 0.2์ดˆ ~ 0.4์ดˆ
  • ๊ธฐ๋ณธ์ ์ธ OG ํƒœ๊ทธ(title, description, image) ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ
  • ๋น ๋ฅด๊ณ  ๊ฐ€๋ฒผ์›€

์˜ˆ์‹œ ์ฝ”๋“œ

import requests
from bs4 import BeautifulSoup

def fetch_og_static(url: str) -> dict:
    headers = {
        "User-Agent": "Mozilla/5.0"
    }

    try:
        response = requests.get(url, headers=headers, timeout=5)
        response.raise_for_status()

        soup = BeautifulSoup(response.text, 'html.parser')

        def get_meta(property_name):
            tag = soup.find("meta", property=property_name)
            return tag["content"] if tag and tag.get("content") else ""

        og_data = {
            "title": get_meta("og:title"),
            "description": get_meta("og:description"),
            "image": get_meta("og:image"),
        }

        return og_data
    except Exception as e:
        print(f"[ERROR] Failed to fetch OG data: {e}")
        return {}

ํ•˜์ง€๋งŒ ํฐ ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

JavaScript๋กœ ๋ Œ๋”๋ง๋˜๋Š” OG ํƒœ๊ทธ๋‚˜ SPA ๊ตฌ์กฐ์˜ ํŽ˜์ด์ง€์—์„œ๋Š” meta ์ •๋ณด๊ฐ€ ์•„์˜ˆ ํฌํ•จ๋˜์ง€ ์•Š๊ฑฐ๋‚˜,
์ดˆ๊ธฐ ์š”์ฒญ ์‹œ์ ์—๋Š” ์กด์žฌํ•˜์ง€ ์•Š์•„ ์ •์  ํŒŒ์‹ฑ์œผ๋กœ๋Š” OG ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์—ˆ์Šต๋‹ˆ๋‹ค.

์ด๋Ÿฐ ๊ฒฝ์šฐ๋Š” ์•„๋ฌด๋ฆฌ ๋น ๋ฅด๊ฒŒ ์ฒ˜๋ฆฌํ•ด๋„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†๋Š” ์ •๋ณด์˜€์Šต๋‹ˆ๋‹ค.


2. ๐Ÿง  ์ •์  ํฌ๋กค๋ง ์‹คํŒจ ์‹œ โ†’ ๋™์  ํฌ๋กค๋ง

์ •์  ํฌ๋กค๋ง๋งŒ์œผ๋กœ๋Š” ํ•œ๊ณ„๊ฐ€ ์žˆ๋‹ค๋Š” ๊ฑธ ๋น ๋ฅด๊ฒŒ ์ฒด๊ฐํ•˜๊ณ ,
์šฐ๋ฆฌ๋Š” ๋™์  ํฌ๋กค๋ง์„ ๋ณ‘๋ ฌ ๊ตฌ์กฐ๋กœ ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ •์  ํฌ๋กค๋ง์ด ์‹คํŒจํ•˜๋ฉด, ๋™์  ํฌ๋กค๋ง์œผ๋กœ ์ž๋™ ์ „ํ™˜

๋™์  ํฌ๋กค๋ง์€ Playwright๋ฅผ ์‚ฌ์šฉํ•ด ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
์‹ค์ œ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ๋„์›Œ ํŽ˜์ด์ง€๋ฅผ ๋ Œ๋”๋งํ•œ ๋’ค, ๋ Œ๋”๋œ HTML์—์„œ OG ๋ฐ์ดํ„ฐ๋ฅผ ํŒŒ์‹ฑํ•ฉ๋‹ˆ๋‹ค.

์‚ฌ์šฉํ•œ ์Šคํƒ

  • ์–ธ์–ด: Python
  • ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ: playwright

์˜ˆ์‹œ ์ฝ”๋“œ

from playwright.sync_api import sync_playwright

def fetch_og_with_playwright(url: str) -> dict:
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()

        try:
            page.goto(url, wait_until="domcontentloaded", timeout=8000)

            og_data = {
                "title": page.locator("meta[property='og:title']").get_attribute("content") or "",
                "description": page.locator("meta[property='og:description']").get_attribute("content") or "",
                "image": page.locator("meta[property='og:image']").get_attribute("content") or "",
            }

            return og_data
        except Exception as e:
            print(f"[ERROR] Failed to fetch OG data: {e}")
            return {}
        finally:
            browser.close()

3. ๐Ÿ” ํ”„๋ก์‹œ ํฌ๋กค๋ง์œผ๋กœ ์šฐํšŒํ•˜๊ธฐ

์ •์ /๋™์  ํฌ๋กค๋ง ๊ตฌ์กฐ๋ฅผ ๊ฐ–์ท„์ง€๋งŒ, ์—ฌ์ „ํžˆ ํ•œ ๊ฐ€์ง€ ๋ฌธ์ œ๊ฐ€ ๋‚จ์•„ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
๋ฐ”๋กœ โ€œ๋ณด์•ˆ์ด ์ฒ ์ €ํ•œ ์‚ฌ์ดํŠธโ€์—์„œ๋Š” ํฌ๋กค๋ง ์ž์ฒด๊ฐ€ ์ฐจ๋‹จ๋œ๋‹ค๋Š” ์ ์ž…๋‹ˆ๋‹ค.

๋Œ€ํ‘œ์ ์œผ๋กœ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์‚ฌ์ดํŠธ๋“ค์ž…๋‹ˆ๋‹ค:

  • Cloudflare, Incapsula ๋“ฑ WAF(Web Application Firewall)๊ฐ€ ์ ์šฉ๋œ ์‚ฌ์ดํŠธ
  • ์ ‘๊ทผ ๋นˆ๋„๋‚˜ User-Agent ๋“ฑ์„ ๋ถ„์„ํ•ด ๋ด‡ ํŠธ๋ž˜ํ”ฝ์„ ์ฐจ๋‹จํ•˜๋Š” ์„œ๋น„์Šค
  • IP๋‚˜ ๊ตญ๊ฐ€ ๊ธฐ๋ฐ˜ ์ฐจ๋‹จ ์ •์ฑ…์ด ์žˆ๋Š” ์ผ๋ถ€ ๊ธ€๋กœ๋ฒŒ ๋„๋ฉ”์ธ

์ด๋Ÿฐ ์‚ฌ์ดํŠธ๋Š” ๋‹จ์ˆœํ•œ User-Agent ์กฐ์ž‘๋งŒ์œผ๋กœ๋Š” ํ†ตํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ๋„์ž…ํ•œ ๊ฒƒ์ด ๋ฐ”๋กœ Smartproxy์ž…๋‹ˆ๋‹ค.


๐Ÿ’ก ์™œ Smartproxy๋ฅผ ์‚ฌ์šฉํ–ˆ๋‚˜์š”?

Smartproxy๋Š” ์ €ํฌ๊ฐ€ ํ…Œ์ŠคํŠธํ•œ ์—ฌ๋Ÿฌ ํ”„๋ก์‹œ ์„œ๋น„์Šค ์ค‘์—์„œ
๊ฐ€์žฅ ์•ˆ์ •์ ์ด๊ณ  ๋น ๋ฅธ ์†๋„, ๊ทธ๋ฆฌ๊ณ  ์š”๊ธˆ ๊ตฌ์กฐ์˜ ์œ ์—ฐ์„ฑ์„ ๊ฐ–์ถ˜ ์„œ๋น„์Šค์˜€์Šต๋‹ˆ๋‹ค.

  • โœ… ์„ฑ๊ณตํ•œ ์š”์ฒญ์— ๋Œ€ํ•ด์„œ๋งŒ ๋น„์šฉ์ด ๋ถ€๊ณผ๋จ
  • โœ… ์ „ ์„ธ๊ณ„ ์ˆ˜์‹ญ๋งŒ ๊ฐœ์˜ IP ํ’€ ์ œ๊ณต (ํšŒ์ „ํ˜•, ๊ณ ์ •ํ˜• ๋ชจ๋‘ ์ง€์›)
  • โœ… ๋‹ค๋ฅธ ํ”„๋ก์‹œ๋ณด๋‹ค ์‘๋‹ต ์†๋„์™€ ์„ฑ๊ณต๋ฅ ์ด ์šฐ์ˆ˜
  • โœ… Python, Node.js ๋“ฑ ๋‹ค์–‘ํ•œ ์–ธ์–ด์—์„œ ์‰ฝ๊ฒŒ ์—ฐ๋™ ๊ฐ€๋Šฅ

์ด๋Ÿฐ ํŠน์„ฑ ๋•๋ถ„์— ํŠน์ • ์‚ฌ์ดํŠธ๋Š” ํ”„๋ก์‹œ ํฌ๋กค๋ง์œผ๋กœ๋งŒ ์ ‘๊ทผํ•˜๋„๋ก ๊ตฌ์กฐ๋ฅผ ๋ถ„๊ธฐํ–ˆ์Šต๋‹ˆ๋‹ค.


๐Ÿงช ์˜ˆ์‹œ ์ฝ”๋“œ (Smartproxy + Python)

Smartproxy๋Š” HTTP ํ”„๋ก์‹œ ๋ฐฉ์‹์œผ๋กœ ์ ‘๊ทผํ•˜๋ฏ€๋กœ, requests์—์„œ ์†์‰ฝ๊ฒŒ ์—ฐ๋™ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import requests

# ์‹ค์ œ ์‘๋‹ต body๋ฅผ BeautifulSoup ๋“ฑ์œผ๋กœ ํŒŒ์‹ฑํ•˜์—ฌ OG ๋ฐ์ดํ„ฐ ์ถ”์ถœ ๊ฐ€๋Šฅ
# ์˜ˆ: soup = BeautifulSoup(response.text, 'html.parser')
def fetch_with_proxy(url: str) -> dict:
    proxies = {
        "http": "http://username:password@gate.smartproxy.com:7000",
        "https": "http://username:password@gate.smartproxy.com:7000"
    }

    headers = {
        "User-Agent": "Mozilla/5.0"
    }

    try:
        response = requests.get(url, headers=headers, proxies=proxies, timeout=10)
        response.raise_for_status()

        # ์—ฌ๊ธฐ์„œ๋Š” ๊ฐ„๋‹จํžˆ ์ƒํƒœ ์ฝ”๋“œ๋งŒ ๋ฐ˜ํ™˜ (์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” BeautifulSoup ๋“ฑ์œผ๋กœ ํŒŒ์‹ฑ)
        return {
            "status": response.status_code,
            "preview": response.text[:200]  # ์ผ๋ถ€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ์šฉ
        }
    except Exception as e:
        print(f"[ERROR] Proxy fetch failed: {e}")
        return {}

โœ… ์ตœ์ข… ๊ตฌ์กฐ ์š”์•ฝ

  1. ์ •์  ํฌ๋กค๋ง โ†’ ๋น ๋ฅด๊ณ  ๊ฐ€๋ณ์ง€๋งŒ ํ•œ๊ณ„๊ฐ€ ์žˆ์Œ
  2. ์ •์  ์‹คํŒจ ์‹œ โ†’ ๋™์  ํฌ๋กค๋ง (Playwright๋กœ ๋ Œ๋”๋ง)
  3. ๋ณด์•ˆ ๊ฐ•ํ™”๋œ ์‚ฌ์ดํŠธ โ†’ ํ”„๋ก์‹œ๋กœ ์šฐํšŒ ์‹œ๋„

์ด ๋ชจ๋“  ๊ณผ์ •์„ ๊ฑฐ์ณ,
๋งํฌ ์ €์žฅ ์‹œ ์ธ๋„ค์ผ๊ณผ ์ œ๋ชฉ์ด ๋น ๋ฅด๊ฒŒ ์ž˜ ๋œจ๋Š” ๊ตฌ์กฐ๋ฅผ ์™„์„ฑํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.


๐Ÿงช ์ง€๊ธˆ, ๋ฒ ํƒ€ ๋ฒ„์ „์—์„œ ์ฒดํ—˜ํ•ด๋ณด์„ธ์š”!

์šฐ๋ฆฌ๋Š” ์ง€๊ธˆ ๋งํฌ ๋“œ๋ผํผ๋ผ๋Š” ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
์—ฌ๋Ÿฌ๋ถ„์ด ์ €์žฅํ•œ ๋งํฌ๋ฅผ ๋” ์ž˜ ์ •๋ฆฌํ•˜๊ณ , ๋‹ค์‹œ ๊บผ๋‚ด๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ๋•๋Š” ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค.

๐Ÿ‘‰ ๐Ÿ”— ๋งํฌ ๋“œ๋ผํผ ๋ฒ ํƒ€ ์ฒดํ—˜ํ•˜๋Ÿฌ ๊ฐ€๊ธฐ

์ง€๊ธˆ ์ง์ ‘ ์ €์žฅํ•ด๋ณด์‹œ๊ณ , ์ธ๋„ค์ผ์ด ์–ด๋–ป๊ฒŒ ์ž˜ ๋‚˜์˜ค๋Š”์ง€ ํ™•์ธํ•ด๋ณด์„ธ์š”!


๐Ÿ“Œ ๋‹ค์Œ ๊ธ€ ์˜ˆ๊ณ 

์—ฌ์ „ํžˆ ์ €ํฌ๋Š” ๋งŒ์กฑ์Šค๋Ÿฌ์šด ํผํฌ๋จผ์Šค๋ฅผ ์–ป์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.
๋‹ค์Œ ๊ธ€์—์„œ๋Š” โ€œOpen Graph ํฌ๋กค๋ง ์†๋„๋ฅผ ์–ด๋–ป๊ฒŒ ๊ฐœ์„ ํ–ˆ๋Š”๊ฐ€โ€์— ๋Œ€ํ•œ ์ด์•ผ๊ธฐ๋ฅผ ๋‚˜๋ˆ ๋ณด๋ ค ํ•ฉ๋‹ˆ๋‹ค.

์ฝ์–ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค ๐Ÿ™Œ
๊ถ๊ธˆํ•œ ์ ์ด๋‚˜ ์˜๊ฒฌ์€ ๋Œ“๊ธ€๋กœ ํŽธํ•˜๊ฒŒ ๋‚จ๊ฒจ์ฃผ์„ธ์š”!

profile
โ€œ๊ธฐ๋กํ•˜๋Š” ์Šต๊ด€์„ ๋„๊ตฌ๋กœ ๋งŒ๋“ค๋‹ค โ€” ๋‘ ๊ฐœ๋ฐœ์ž์˜ ๋งํฌ ๋“œ๋ผํผ ๊ตฌ์ถ•๊ธฐโ€

3๊ฐœ์˜ ๋Œ“๊ธ€

comment-user-thumbnail
2025๋…„ 5์›” 3์ผ

ํ›Œ๋ฅญํ•œ ๊ธ€๊ณผ ์ข‹์€ ํ”„๋กœ๋•ํŠธ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค ! ๋„ˆ๋ฌด ์จ๋ณด๊ณ  ์‹ถ์€๋ฐ ์นด์นด์˜ค๋กœ ํšŒ์›๊ฐ€์ž…์„ ์‹œ๋„ํ•˜๋ฉด 500์—๋Ÿฌ์™€ ํ•จ๊ป˜ ํšŒ์›๊ฐ€์ž…์ด ์ด๋ค„์ง€์ง€ ์•Š๋„ค์š” ใ… ใ… 

2๊ฐœ์˜ ๋‹ต๊ธ€