브랜드는 사람들이 자사와 자사 제품, 그리고 경쟁사에 대해 어떤 이야기를 하는지 알고 싶어 하며, 그런 대화의 상당 부분은 공개적으로 이루어진다: 포럼, 리뷰 사이트, 커뮤니티 게시판, 공개 소셜 프로필 등이다. 이를 수집하는 것이 소셜 리스닝의 기본이며, 보기보다 어렵다. 플랫폼마다 페이지 구조가 다르고, 많은 경우 JavaScript로 콘텐츠를 렌더링하며, 수집하는 거의 모든 것이 사람과 관련되어 있다.
이 가이드는 오늘날 팀들이 웹 스크래핑 API로 공개 소셜 데이터를 수집하는 방법을 다룬다: 어떤 소스를 사용할지, 언제 공식 API가 더 나은 경로인지, 서로 다른 플랫폼을 하나의 데이터셋으로 통합하는 방법, 그리고 처음부터 개인 데이터를 배제하는 방법이다.
무엇을 알고자 하는지부터 정하라
소셜 데이터 프로젝트는 모든 것을 수집한 뒤 나중에 결정하려 할 때 실패한다. 먼저 어떤 질문에 답할 것인지 정하라:
- 규모(Volume): 브랜드가 얼마나 자주 언급되며, 그 빈도가 변하고 있는가?
- 주제(Topics): 사람들이 어떤 제품이나 이슈에 대해 무엇을 말하고 있는가?
- 비교(Comparison): 브랜드 언급이 경쟁사의 언급과 비교해 어떠한가?
- 사건(Incidents): 불만이나 위조품 게시물이 확산되고 있는가?
각 질문에는 몇 개의 필드만 있으면 된다: 게시물의 링크, 시간, 텍스트, 그리고 출처. 게시자에 대한 더 많은 정보가 필요한 프로젝트는 거의 없다.
소스 경로를 선택하라
| 소스 | 최선의 경로 |
|---|---|
| 필요를 충족하는 공식 API가 있는 플랫폼 | 공식 API, 약관 범위 내에서 |
| 공개 포럼, 커뮤니티 게시판, 리뷰 사이트 | 공개 페이지 수집 |
| JavaScript로 렌더링되는 페이지 | 렌더링을 지원하는 웹 스크래핑 API |
| 로그인이 필요한 모든 것 | 수집하지 말 것 |
공식 API가 존재하고 수집 규모와 예산에 맞는다면 보통 가장 신뢰할 수 있고 명확하게 허용되는 경로다. 공식 API가 없는 공개 페이지의 경우, 웹 스크래핑 API가 소셜 사이트를 까다롭게 만드는 부분들을 처리해준다: JavaScript 렌더링, 콘텐츠 로딩 대기, 특정 국가에서 보이는 그대로의 페이지 가져오기 등이다. 추출 규칙은 필요한 필드를 원시 HTML이 아닌 JSON으로 반환한다.
모든 사이트에서 공개된 영역에만 머물러라. 로그인 뒤에 있는 콘텐츠, 비공개 그룹, 다이렉트 메시지는 접근 대상이 아니다. 이는 접근 자체가 수집을 금지하는 약관 동의를 수반하는 경우가 많기 때문이고, 또한 관련된 사람들이 사생활을 기대하기 때문이다.
모든 플랫폼을 위한 하나의 스키마
각 소스는 필드 이름을 다르게 붙인다: 포럼에는 permalink와 body가 있고, 리뷰 사이트에는 link와 review text가 있으며, 날짜는 형식과 시간대가 제각각이다. 이를 따로 분석하면 비교가 깨진다. 수집하면서 모든 소스를 하나의 작은 스키마에 매핑하라:
- source: 게시물의 출처;
- url: 정규화된 주소. 같은 게시물이 한 번만 집계되도록 함;
- published_at: UTC 기준 시간;
- text: 공백이 정규화된 게시물 텍스트;
- author_key: 작성자 이름 대신 쓰는 가명 키.
아래 모듈은 이 매핑을 수행하고, 중복 URL(추적 파라미터와 프래그먼트 때문에 하나의 게시물이 여러 개처럼 보이는 경우가 많다)을 제거하며, 브랜드별 일일 언급 수를 집계한다:
import hashlib
from collections import Counter
from datetime import datetime, timezone
from urllib.parse import urlsplit, urlunsplit
# Where each source keeps the fields we need, after extraction rules have run.
FIELD_MAPS = {
"forum": {"url": "permalink", "text": "body", "author": "username", "time": "created"},
"review": {"url": "link", "text": "review_text", "author": "reviewer", "time": "date"},
}
def canonical(url):
"""Drop query strings and fragments so the same post counts once."""
parts = urlsplit(url)
return urlunsplit((parts.scheme, parts.netloc.lower(), parts.path.rstrip("/"), "", ""))
def normalise(source, raw, salt):
"""One post in a common shape, with the author pseudonymised and no other personal data."""
m = FIELD_MAPS[source]
when = datetime.fromisoformat(raw[m["time"]].replace("Z", "+00:00")).astimezone(timezone.utc)
return {
"source": source,
"url": canonical(raw[m["url"]]),
"published_at": when.isoformat(timespec="seconds"),
"text": " ".join(raw[m["text"]].split()),
"author_key": hashlib.sha256((salt + raw[m["author"]]).encode()).hexdigest()[:16],
}
def daily_mentions(posts, brands):
"""Count posts per day that mention each brand, counting each URL once."""
seen, counts = set(), Counter()
for post in posts:
if post["url"] in seen:
continue
seen.add(post["url"])
day = post["published_at"][:10]
for brand in brands:
if brand.lower() in post["text"].lower():
counts[(day, brand)] += 1
return dict(sorted(counts.items()))
세 개의 예시 레코드에 적용해보면, 그중 하나는 추적 파라미터가 붙은 중복이다:
import os
from social import daily_mentions, normalise
salt = os.environ.get("MENTIONS_SALT", "rotate-me-per-project")
raw = [
("forum", {"permalink": "https://forum.example/t/88?ref=home", "body": "Acme kettle died after a week",
"username": "kitchen_fan", "created": "2026-10-20T08:15:00Z"}),
("forum", {"permalink": "https://forum.example/t/88", "body": "Acme kettle died after a week",
"username": "kitchen_fan", "created": "2026-10-20T08:15:00Z"}),
("review", {"link": "https://reviews.example/p/42", "review_text": "Better than my old Brandly one",
"reviewer": "J. D.", "date": "2026-10-20T19:40:00+02:00"}),
]
posts = [normalise(source, item, salt) for source, item in raw]
print(daily_mentions(posts, ["Acme", "Brandly"]))
{('2026-10-20', 'Acme'): 1, ('2026-10-20', 'Brandly'): 1}
중복은 한 번만 집계되고, 리뷰의 현지 시간은 UTC로 변환되며, 어떤 사용자 이름도 결과에 나타나지 않는다.
개인 데이터는 처음부터 배제한다
공개 게시물이라 해도 유럽을 비롯한 많은 지역에서는 여전히 개인 데이터이며, 게시물이 아무리 공개적이었어도 데이터 보호 규정이 적용된다. 가장 안전한 설계는 질문에 필요한 만큼만 사람에 대한 정보를 최소한으로 남기는 것이다:
- 작성자를 가명화하라. 솔트를 더한 해시를 사용하면 사용자 이름을 저장하지 않고도 서로 다른 게시자 수를 세거나 한 계정이 반복 게시하는 것을 포착할 수 있다. 프로젝트마다 다른 솔트를 사용하고, 이를 비밀로 유지하라.
- 프로필을 수집하지 마라. 개인의 팔로워 수, 소개글, 사진, 위치 정보는 브랜드 관련 질문에 필요한 경우가 거의 없다.
- 보관 기간을 짧게 유지하라. 집계된 수치와 주제는 보관할 수 있지만, 원시 텍스트는 보통 분석 후 삭제할 수 있다.
- 집계 형태로 보고하라. 사람을 특정하지 않고 추세와 사례를 공개하라.
소셜 플랫폼을 감정 분석을 위해 스크래핑하기 가이드는 데이터가 정리된 이후 분석 단계를 다루며, 온라인에서 브랜드 언급 추적하기 가이드는 쿼리 세트를 구축하고 지역별 차이를 다루는 법을 설명한다.
조심스럽게 수집하라
소셜 및 커뮤니티 사이트는 사용자들을 위해 운영된다. 사람이 둘러보는 속도에 맞춰 수집하고, 전체 아카이브를 크롤링하기보다 새 게시물을 나열하는 페이지를 우선시하며, 중단을 요청하는 소스로부터는 수집을 멈춰라. 브랜드 모니터링은 중요한 소스에 대한 하루 한 번의 수집으로 충분한 경우가 대부분이다.
FAQ
소셜 미디어 데이터를 수집하는 것이 합법인가?
공개 게시물을 수집하는 것은 대체로 합법이지만, 플랫폼 약관, 데이터에 접근하는 방식, 데이터 보호법이 모두 적용된다. 적합한 경우 공식 API를 사용하고, 로그인 뒤의 콘텐츠는 피하며, 목적에 필요한 수준으로만 개인 데이터를 유지하라. 구체적인 사안에 대해서는 법률 자문을 받으라.
플랫폼의 API와 웹 스크래핑 API 중 무엇을 사용해야 하는가?
필요한 데이터와 규모를 약관 범위 내에서 충족하는 공식 API가 있다면 그것을 사용하라. 적합한 API가 없는 공개 페이지, 특히 JavaScript로 렌더링되는 페이지에는 웹 스크래핑 API를 사용하라.
소셜 미디어에서 개인 데이터를 수집하지 않으려면 어떻게 해야 하는가?
질문에 필요한 필드만 수집하고, 사용자 이름을 솔트를 더한 가명 키로 대체하며, 프로필은 아예 건너뛰고, 원시 텍스트는 필요한 기간만 보관하고, 결과는 집계 형태로 보고하라.
소셜 언급은 얼마나 자주 수집해야 하는가?
대부분의 브랜드 모니터링에는 매일 수집으로 충분하다. 출시나 사건이 있을 때만 더 자주 수집하고, 실제로 대상 고객이 게시물을 올리는 소스에 집중하라.
결론
소셜 데이터는 실제 사람들이 공개적으로 하는 말이기 때문에 가치가 있으며, 바로 그 이유로 주의가 필요하다. 먼저 질문을 정하고, 적합한 경우 공식 API를, 공개 페이지에는 웹 스크래핑 API를 사용하며, 모든 소스를 하나의 작은 스키마로 매핑하고, 처음부터 개인 데이터를 배제하도록 설계하라. 그 결과는 필요 이상으로 수집하지 않으면서도 브랜드 관련 질문에 답하는 데이터셋이다.