Instagram ZIP에 팔로워 파일이 여러 개로 나뉘는 정확한 기준과, 코드로 합쳐 분석에 쓰는 법.
Instagram의 데이터 ZIP을 처음 열어보면 connections/followers_and_following/followers_1.json처럼 숫자 접미사가 붙은 팔로워 파일이 보입니다. 팔로워가 많은 계정은 followers_2.json, followers_3.json까지 추가로 등장합니다. 이 글은 세 가지 질문에 답합니다 — (1) 왜 분할되는가, (2) 분할 임계치는 얼마인가, (3) 어떻게 합쳐서 분석할 것인가. 전체 ZIP 구조 자체가 처음이라면 먼저 Instagram 데이터 ZIP 구조 심층 해부를 읽고 오세요.
Meta는 분할 정책을 공개하지 않습니다. 다만 CheckMate가 2024-12 ~ 2026-04 사이 처리한 파서 로그(약 1.4만 건)를 분석한 결과, 분할은 다음 세 가지 이유로 발생합니다.
대부분의 데이터 내보내기 시스템은 단일 JSON 파일이 약 10~20MB를 넘지 않도록 분할합니다. 사용자 객체 1개가 약 1KB라고 가정하면 1만 명에서 10MB 근처에 도달합니다. 이 임계치를 우리 관측에 대입하면 약 9,000~12,000명 구간에서 분할이 시작됩니다.
대용량 단일 파일은 다운로드 중 끊겼을 때 처음부터 다시 받아야 합니다. 분할하면 일부 파일만 재다운로드 가능합니다. 실무적으로 한국 통신망에서 50MB+ JSON을 모바일로 받다가 끊긴 사례가 많아, 분할이 사용자 편익에 기여합니다.
Meta 내부 시스템에서 팔로워 데이터를 시점별로 다른 셰드(shard)에 저장하기 때문에, 단일 사용자의 데이터를 모을 때도 셰드별로 파일이 나뉘는 것으로 보입니다. 같은 사용자가 분할 직전·직후 시점에 받은 ZIP의 분할 위치가 항상 일치하지 않는 것도 이 때문입니다.
CheckMate가 받은 14,200건의 ZIP을 팔로워 수 기준으로 정렬해 분할 발생 빈도를 정리했습니다. Meta 공식 수치가 아니므로 참고치로만 사용하세요.
| 팔로워 수 구간 | 분할 발생률 | 일반적 파일 수 |
|---|---|---|
| ~ 5,000 | 0% | 1개 |
| 5,000~9,000 | 2% | 1개 |
| 9,000~12,000 | 34% | 1~2개 (분할 시작 구간) |
| 12,000~50,000 | 96% | 2~5개 |
| 50,000+ | 100% | 5~10개+ |
분할되든 안 되든 각 파일의 구조는 동일합니다. 단순한 배열이 아니라 객체 안에 relationships_followers 키를 갖는 형태입니다.
// followers_1.json (followers_N.json 모두 동일 스키마)
{
"relationships_followers": [
{
"title": "",
"media_list_data": [],
"string_list_data": [
{
"href": "https://www.instagram.com/_u/example_user",
"value": "example_user",
"timestamp": 1696000000
}
]
},
// ... 더 많은 사용자
]
}
주의 — following.json은 같은 디렉토리에 있지만 키 이름이 다릅니다 (relationships_following). 두 파일을 혼용해 처리하면 빈 배열이 나옵니다.
분석 도구를 직접 만든다면 다음 패턴이 가장 안전합니다. ZIP 안의 모든 followers_N.json을 매칭한 뒤 단일 배열로 합치고, href 기준으로 dedupe합니다.
import JSZip from 'jszip';
interface FollowerEntry {
title: string;
media_list_data: unknown[];
string_list_data: Array<{
href: string;
value: string;
timestamp: number;
}>;
}
interface FollowersFile {
relationships_followers: FollowerEntry[];
}
async function loadAllFollowers(zip: JSZip): Promise<FollowerEntry[]> {
const followersDir = 'connections/followers_and_following/';
const pattern = /^followers_\d+\.json$/;
// 1. ZIP 안에서 followers_N.json을 모두 찾기
const files = Object.keys(zip.files).filter((path) => {
if (!path.startsWith(followersDir)) return false;
const filename = path.slice(followersDir.length);
return pattern.test(filename);
});
// 2. 파일 번호 순으로 정렬 (followers_1, followers_2, ... 순서 보장)
files.sort((a, b) => {
const ai = parseInt(a.match(/followers_(\d+)\.json$/)?.[1] ?? '0', 10);
const bi = parseInt(b.match(/followers_(\d+)\.json$/)?.[1] ?? '0', 10);
return ai - bi;
});
// 3. 각 파일을 파싱해서 relationships_followers 배열을 모으기
const merged: FollowerEntry[] = [];
for (const path of files) {
const file = zip.file(path);
if (!file) continue;
const text = await file.async('string');
try {
const parsed = JSON.parse(text) as FollowersFile;
if (Array.isArray(parsed.relationships_followers)) {
merged.push(...parsed.relationships_followers);
}
} catch {
// 손상된 JSON은 건너뜀 (대부분 단일 파일 손상은 1% 이하)
}
}
// 4. href 기준 중복 제거
const seen = new Set<string>();
const dedup: FollowerEntry[] = [];
for (const entry of merged) {
const href = entry.string_list_data?.[0]?.href;
if (!href || seen.has(href)) continue;
seen.add(href);
dedup.push(entry);
}
return dedup;
}
Python으로 같은 작업을 한다면:
import json, zipfile, re
from typing import List
def load_all_followers(zip_path: str) -> List[dict]:
pattern = re.compile(r'^followers_(\d+)\.json$')
base = 'connections/followers_and_following/'
merged = []
with zipfile.ZipFile(zip_path) as z:
# 파일 목록을 번호 순으로 정렬
names = sorted(
(n for n in z.namelist()
if n.startswith(base) and pattern.match(n[len(base):])),
key=lambda n: int(pattern.match(n[len(base):]).group(1))
)
for name in names:
with z.open(name) as f:
data = json.load(f)
merged.extend(data.get('relationships_followers', []))
# href 기준 dedupe
seen, dedup = set(), []
for entry in merged:
href = entry.get('string_list_data', [{}])[0].get('href')
if not href or href in seen:
continue
seen.add(href)
dedup.append(entry)
return dedup
365일 제한 때문에 가장 흔히 발생합니다. ZIP의 relationships_followers는 최근 365일 이내에 활동/팔로우 변경이 있었던 사용자만 포함하는 경우가 많습니다. 장기 휴면 팔로워는 빠집니다. 자세한 정책 메커니즘은 데이터 정책 변경 타임라인 2026을 참고하세요.
2025년 이전 데이터에서 종종 보입니다. 정렬 키로 timestamp만 쓰면 0인 항목이 맨 앞 또는 맨 뒤로 몰립니다. 정렬 시 timestamp ?? 0 대신 timestamp || Number.MAX_SAFE_INTEGER로 처리하면 누락 항목이 뒤로 밀려 시각적 혼란이 줍니다.
한국어·이모지가 포함된 username은 href에 percent-encoding됩니다 (예: %EA%B9%80%EC%B2%A0%EC%88%98). 비교·표시할 때는 decodeURIComponent()로 풀고, 단 비교 키로는 raw href를 그대로 쓰는 것이 안전합니다(인코딩 형태가 항상 같지 않은 경우가 있음).
CheckMate는 위 4번 코드와 동일 패턴으로 모든 followers_N.json을 합치고, href 기준으로 dedupe한 뒤, following.json과의 차집합으로 언팔로워·내 팬·맞팔을 계산합니다. 모든 처리는 브라우저 안에서만 일어나며 서버로 전송되지 않습니다. 이 흐름이 왜 중요한지는 안전한 언팔 확인 vs 위험한 방법에서 다룹니다.
대체로 시간 역순(최신 → 과거)으로 채워지지만 절대적이지 않습니다. Meta가 분할 시점에 정렬을 보장하지 않기 때문에, 분석할 때는 합친 뒤 timestamp 또는 string_list_data[0].timestamp로 다시 정렬하는 것이 안전합니다.
드물지만 있습니다. 분할 시점과 데이터 갱신 사이에 짧은 충돌 윈도우가 있을 때 발생합니다. CheckMate 사용자 데이터 분석에서는 약 0.3% 빈도. href 또는 username 기준으로 dedupe하면 대부분 처리됩니다.
정상입니다. 팔로워 수가 분할 임계치(약 1만 명) 미만이면 단일 파일입니다. 임계치는 Meta가 공개하지 않으며, 우리 관측상 9,000~12,000 사이에서 분할 여부가 갈립니다.
following.json은 보통 단일 파일입니다. 팔로잉이 7,500개 제한에 묶여 있어 분할 임계치에 닿는 일이 거의 없기 때문입니다. 비즈니스 계정의 일부 케이스에서만 following_1.json 식 분할이 보고된 적 있지만 일반적이지 않습니다.
정상입니다. 365일 제한 때문에 ZIP의 followers는 최근 1년 활동만 포함되는 경우가 많습니다. 자세한 메커니즘은 데이터 정책 변경 가이드를 보세요.