API 데이터 수집할 때 샘플 단위로 처리해야 하는 이유 | 열 단위 수집이 위험한 이유

API 응답을 DataFrame으로 만들 때 가장 중요한 건 데이터를 얼마나 많이 가져오느냐보다, 각 값이 같은 행에 정확히 묶여 들어가느냐다.

그래서 실전 수집에서는 컬럼별로 한 번에 긁는 방식보다, 샘플 1개를 하나의 묶음으로 보고 필요한 필드를 추출한 뒤 append하는 방식이 훨씬 안전하다. 자료에서도 JSON과 XML 모두에서 “가로줄 샘플 베이스로 처리하는 것이 답”이라고 설명하고, 감독 정보처럼 없는 값이 있으면 열 단위 수집은 바로 밀릴 수 있다고 정리한다.

이 글의 핵심

이 글은 “API 응답에서 값을 어떻게 꺼내는가”보다 어떻게 쌓아야 데이터가 틀어지지 않는가에 초점이 있다.

즉, 비교 기준은 이거다.

  • 열 단위 수집: movieCd만 쭉, movieNm만 쭉, peopleNm만 쭉
  • 샘플 단위 수집: 영화 1개를 기준으로 필요한 값들을 같이 뽑아 한 줄로 append

겉보기엔 둘 다 가능해 보이지만, 실제로는 두 번째 방식이 훨씬 안전하다.


왜 열 단위 수집이 위험할까

처음 보면 이런 생각을 하기 쉽다.

  • 영화 코드만 다 모으면 되지 않을까
  • 제목만 쭉 뽑으면 되지 않을까
  • 감독 이름도 peopleNm만 다 찾으면 되지 않을까

문제는 모든 필드가 항상 같은 개수로 존재하지는 않는다는 데 있다.

자료에서도 JSON 응답에서 어떤 영화는 directors가 []라 첫 번째 감독 이름을 바로 뽑으려 하면 IndexError가 발생하는 예시가 나온다. 즉, 영화는 50개인데 감독 이름은 50개가 아닐 수 있다.

XML도 마찬가지다. 자료에서는 movieCd는 영화마다 있지만 peopleNm은 없는 영화도 있어서, peopleNm만 한 번에 다 찾으면 영화 row와 감독 row의 개수가 맞지 않을 수 있다고 설명한다. 특히 녹취에서는 이런 방식으로 값을 따로 긁으면 “이빨이 밀릴 수 있다”고 직접 지적한다.


열 단위 수집이 왜 “밀리는지” 예시로 보면

예를 들어 영화 3개가 있다고 하자.

movies = [
    {"movieCd": "1", "movieNm": "A", "directors": [{"peopleNm": "감독A"}]},
    {"movieCd": "2", "movieNm": "B", "directors": []},
    {"movieCd": "3", "movieNm": "C", "directors": [{"peopleNm": "감독C"}]}
]

이 상태에서 열 단위로 접근하면 이렇게 생각하기 쉽다.

movie_codes = [m["movieCd"] for m in movies]
movie_names = [m["movieNm"] for m in movies]
director_names = [m["directors"][0]["peopleNm"] for m in movies if m["directors"] != []]

결과는 대략 이렇게 된다.

movie_codes   # ["1", "2", "3"]
movie_names   # ["A", "B", "C"]
director_names # ["감독A", "감독C"]

문제는 여기서 시작된다.

  • 영화 코드는 3개
  • 제목도 3개
  • 감독명은 2개

이 상태에서 옆으로 붙이면, 어떤 감독명이 어떤 영화에 붙어야 하는지 보장이 깨진다.

즉, 열 단위 수집은 “없는 값”이 등장하는 순간 바로 불안정해진다.


샘플 단위 수집은 왜 안전할까

샘플 단위 수집은 기준이 다르다.

핵심은 영화 1개를 하나의 row로 보고, 필요한 값을 그 안에서 다 처리한 뒤 한 번에 append하는 것이다.

자료에서도 JSON 수집 예시에서 “항상 수집은 1개 샘플에서 필요한 정보들을 추출하는 스타일”이라고 명시하고, 세로줄 컬럼 베이스가 아니라 가로줄 샘플 베이스로 처리해야 데이터가 밀리지 않는다고 설명한다.


JSON에서는 이렇게 처리한다

KOBIS JSON 예시를 보면 영화 1개는 dict 하나다.

그래서 이 dict를 샘플 1개로 보고 필요한 값들을 먼저 뽑은 뒤, 그 결과를 한 줄로 append한다.

tot_data = []

for data in movie_data["movieListResult"]["movieList"]:
    i_code = data["movieCd"]
    i_name = data["movieNm"]
    i_name_e = data["movieNmEn"]
    i_day = data["openDt"]

    if data["directors"] == []:
        i_dir = ""
    else:
        i_dir = data["directors"][0]["peopleNm"]

    tot_data.append([i_code, i_name, i_name_e, i_day, i_dir])

이 방식의 핵심은 간단하다.

  • 영화 1개를 꺼낸다
  • 그 영화의 코드, 제목, 영문제목, 개봉일, 감독명을 같은 맥락 안에서 처리한다
  • 감독이 없으면 그 자리만 빈 문자열로 채운다
  • 마지막에 한 줄로 append한다

즉, 값이 비어도 row 구조는 안 무너진다. 자료에서도 בדיוק 이 구조로 tot_data.append([i_code, i_name, i_name_e, i_day, i_dir])를 사용하는 예시가 나온다.


dict append 방식도 같은 원리다

리스트 대신 dict로 쌓는 방식도 본질은 같다.

차이는 컬럼명을 같이 담느냐 정도다.

tot_data = []

for data in movie_data["movieListResult"]["movieList"]:
    if data["directors"] == []:
        i_dir = ""
    else:
        i_dir = data["directors"][0]["peopleNm"]

    tot_data.append({
        "movieCd": data["movieCd"],
        "movieTitle": data["movieNm"],
        "movieETitle": data["movieNmEn"],
        "openDay": data["openDt"],
        "dirName": i_dir
    })

이 방식도 결국은 샘플 1개 → 필요한 값 1묶음 → append 1번 구조다. 자료에서도 리스트 기반 누적과 dict 기반 누적 두 방식이 모두 나오지만, 둘 다 공통적으로 “샘플 단위”로 처리한다.


XML에서는 샘플 단위 수집이 더 중요하다

XML은 JSON보다 더 조심해야 한다.

왜냐하면 JSON처럼 dict/list 구조가 명확하지 않고, find_all("peopleNm")처럼 태그를 직접 긁는 방식은 더 쉽게 밀릴 수 있기 때문이다.

자료에서도 XML 예시에서 peopleNm만 전체에서 다 찾으면, 감독이 없는 영화는 건너뛰어져서 영화 코드 리스트와 감독명 리스트의 길이가 맞지 않을 수 있다고 설명한다. 녹취에서도 movieCd는 영화마다 있지만 peopleNm은 없는 경우가 있어서, 이런 식으로 따로 긁으면 “옆에 라인이 쪼개질 수 있다”고 한다. 그래서 결국 각 movie 태그를 하나의 샘플로 잡고 그 안에서 필요한 값을 추출해야 한다고 강조한다.


XML은 movie 태그 1개를 샘플로 본다

XML 예시를 샘플 단위로 쓰면 이런 구조가 된다.

movie_list = []

for data in soup.find_all("movie"):
    i_dict = {
        "code": data.find("movieCd").text,
        "title": data.find("movieNm").text,
        "e-title": data.find("movieNmEn").text,
        "openD": data.find("openDt").text,
        "dirName": data.find("peopleNm").text if data.find_all("peopleNm") != [] else "X"
    }

    movie_list.append(i_dict)

여기서 중요한 건 find_all("movie")로 개별 영화 태그를 먼저 잡는다는 점이다.

즉,

  • 전체 XML에서 peopleNm만 다 찾는 게 아니라
  • movie 하나 안에서 movieCd, movieNm, movieNmEn, openDt, peopleNm을 같이 처리한다

그래서 감독이 없더라도 그 영화 row 안에서만 빈값 처리가 되고, 다른 영화와 엉키지 않는다. 자료에서도 XML 수집 예시를 이렇게 for idx, data in enumerate(soup.find_all("movie")): 구조로 풀고 있다.


샘플 단위 수집은 DataFrame 변환도 자연스럽다

샘플 단위로 리스트를 쌓았든 dict를 쌓았든, 마지막에는 DataFrame으로 바꾸기 쉽다.

리스트 기반

df = pd.DataFrame(
    data=tot_data,
    columns=["movieCd", "movieTitle", "movieETitle", "openDay", "dirName"]
)

dict 기반

df = pd.DataFrame(tot_data)

이게 가능한 이유는 이미 append할 때부터 한 샘플 = 한 줄 구조로 정리해두었기 때문이다.

즉, 수집 단계에서 row 구조를 지키면 후처리도 쉬워진다.


열 단위 수집은 언제 괜찮고, 언제 위험할까

열 단위 수집이 괜찮은 경우

  • 모든 필드가 반드시 존재할 때
  • 한 row당 값 개수가 정확히 1:1일 때
  • 누락값이 사실상 없을 때

열 단위 수집이 위험한 경우

  • 중첩 리스트가 있을 때
  • 없는 값이 있을 수 있을 때
  • 사람 이름, 감독, 배우처럼 개수가 달라질 수 있을 때

API 응답은 보통 두 번째 케이스가 많다.

그래서 실제 수집 코드에서는 보수적으로 샘플 단위로 짜는 편이 안전하다.


구현 관점에서 기억할 포인트

1. 수집 기준은 “컬럼”이 아니라 “샘플”이다

영화 1개, 공시 1개, 상품 1개처럼 한 row를 기준으로 처리해야 한다. 자료에서도 “가로줄 샘플 베이스”를 직접 강조한다.

2. 누락값은 row 안에서 처리해야 한다

감독이 없으면 그 row의 감독 칸만 빈값으로 채워야지, 감독 열 전체를 따로 수집하면 안 된다.

3. JSON과 XML 모두 원리는 같다

JSON은 dict 단위, XML은 movie 태그 단위로 보면 된다. 형식은 달라도 샘플 기준이라는 원리는 같다.

4. append 시점에 이미 DataFrame row 구조를 만들어둔다

나중에 정리하려고 하지 말고, append할 때부터 한 줄 구조를 맞춰두는 게 가장 안전하다.


API 데이터 수집은 컬럼별로 따로 긁는 것보다, 샘플 1개에서 필요한 값을 함께 추출해 한 줄씩 append하는 방식이 훨씬 안전하다.