11. LangGraph 입문: 상태 그래프의 3대 요소

🎯 이 장의 목표
  • LangGraph가 왜 "그래프"인지, 기존 SDK와 무엇이 다른지 이해한다
  • 세 가지 핵심 원시 State·Node·Edge를 익힌다
  • 가장 단순한 그래프를 정의하고 컴파일·실행한다
  • 리듀서(reducer)로 상태 업데이트를 제어하는 법을 안다

비유로 시작하기: 공정도(flowchart) 엔진

지금까지의 OpenAI Agents SDK가 "잘 정리된 공구함"이었다면, LangGraph공정도 엔진에 가깝습니다. 작업의 흐름을 그래프(노드와 엣지)로 명시적으로 그려두고, 그 그래프대로 실행합니다.

왜 이런 접근이 필요할까요? 현실의 에이전트는 직선으로 흐르지 않습니다. 고객 메시지를 읽고 → 지식베이스를 검색할지 도구를 부를지 판단하고 → 실패하면 재시도하고 → 사람의 승인을 기다리고 → 전체 대화를 기억해야 합니다. 이런 분기·반복·중단·복구를 깔끔하게 다루려면, 흐름 자체를 그래프로 표현하는 것이 유리합니다.

📌 핵심
최신 정보 (2026): LangGraph는 2026년 5월 1.2가 나왔고, 프로덕션 에이전트의 사실상 표준으로 자리잡았습니다. 핵심 가치는 durable execution(실패 후 정확히 멈춘 지점부터 재개), 체크포인트, human-in-the-loop입니다. Python 3.11/3.12를 권장합니다. 일부 구버전 API(config_schema 등)는 context_schema로 대체되었으니, 오래된 튜토리얼을 볼 때 주의하세요.

설치

BASH
pip install langgraph langchain langchain-openai

세 가지 핵심 원시

LangGraph의 모든 것은 단 세 가지로 환원됩니다.

flowchart LR
    classDef state fill:#A5D6A7,stroke:#2E7D32,color:#000
    classDef node fill:#80DEEA,stroke:#00838F,color:#000
    classDef edge fill:#90CAF9,stroke:#1565C0,color:#000

    S[State<br/>공유 데이터]:::state
    N[Node<br/>일을 하는 함수]:::node
    E[Edge<br/>노드 간 연결]:::edge

    S -.모든 노드가 읽고 씀.-> N
    N -->|Edge로 다음 노드 결정| N

세 요소의 정체는 이렇습니다. State는 그래프 전체가 공유하는 데이터(보통 TypedDict)입니다. TypedDict는 딕셔너리({"키": 값})의 각 키에 어떤 자료형이 들어갈지 미리 선언해 두는 파이썬 타입으로, 상태의 구조를 분명히 해 줍니다. Node는 현재 상태를 받아 일을 하고 바뀐 부분을 돌려주는 함수입니다. Edge는 노드에서 노드로의 전환을 정의합니다. 이 셋을 어떻게 엮느냐에 따라 단순 파이프라인부터 멀티에이전트 시스템까지 모두 표현됩니다.

📌 핵심: 1부에서 배운 "메모리=대화 기록"이 여기서는 State로 격상됩니다. LangGraph에서 메모리는 그래프의 상태이고, 모든 노드가 그 상태를 읽고 씁니다.

가장 단순한 그래프

State를 정의하고, Node 하나를 만들고, START → node → END로 연결합니다.

PYTHON
from typing import TypedDict
from langgraph.graph import StateGraph, START, END

# 1) State 정의 — 그래프가 읽고 쓸 데이터의 스키마
class State(TypedDict):
    question: str
    answer: str

# 2) Node 정의 — 상태를 받아 바뀐 부분만 반환
def answer_node(state: State) -> dict:
    q = state["question"]
    return {"answer": f"'{q}'에 대한 답변입니다."}

# 3) 그래프 조립
builder = StateGraph(State)
builder.add_node("answer", answer_node)
builder.add_edge(START, "answer")   # 시작 → answer 노드
builder.add_edge("answer", END)     # answer 노드 → 끝

# 4) 컴파일 (반드시 필요)
graph = builder.compile()

# 5) 실행
result = graph.invoke({"question": "LangGraph가 뭐야?"})
print(result["answer"])

흐름은 다섯 단계입니다. State 스키마를 정하고, 노드 함수를 만들고, add_node/add_edge로 조립하고, compile()로 실행 가능한 그래프를 만들고, invoke()로 돌립니다.

⚠️ 흔한 실수: compile()을 빼먹는 것. StateGraph는 설계도일 뿐이고, 컴파일해야 실행 가능한 그래프가 됩니다. 컴파일 단계에서 고아 노드 같은 구조적 오류도 검사합니다.

💡 팁: STARTEND는 진짜 노드가 아니라 표식입니다. START에서 나가는 엣지는 "그래프가 여기서 시작한다", END로 가는 엣지는 "여기서 멈춘다"는 의미입니다.

노드는 "바뀐 부분만" 반환한다

중요한 규칙: 노드는 상태 전체를 반환하지 않고, 자기가 바꾼 키만 반환합니다. 위 예에서 answer_nodeanswer만 돌려줬습니다. question은 건드리지 않았으니 그대로 유지됩니다. LangGraph가 반환된 부분을 기존 상태에 병합(merge)합니다.

리듀서: 상태를 어떻게 병합할까

기본 병합 방식은 덮어쓰기(replacement)입니다. 같은 키에 새 값을 반환하면 이전 값을 대체합니다. 그런데 대화 기록처럼 누적해야 하는 값은 어떻게 할까요? 이때 리듀서(reducer)를 씁니다.

PYTHON
from typing import Annotated, TypedDict
import operator

class State(TypedDict):
    # messages는 덮어쓰지 않고 계속 더해진다(누적)
    messages: Annotated[list, operator.add]
    counter: int  # 리듀서 없음 → 덮어쓰기

Annotated[list, operator.add]는 "이 키는 새 값을 기존 리스트에 더하라"는 뜻입니다. 메시지처럼 쌓여야 하는 데이터에 필수입니다. LangGraph는 메시지 누적을 위한 전용 헬퍼 add_messages도 제공합니다.

상황리듀서동작
단일 값(현재 상태·카운터)없음(기본)덮어쓰기
누적 목록(대화 기록)operator.add / add_messages기존에 추가

📌 핵심: 리듀서는 "여러 노드가 같은 키에 쓸 때 어떻게 합칠지"를 정합니다. 1부에서 손으로 history.append(...) 했던 것을, 리듀서가 선언적으로 자동화한 셈입니다.

그래프 시각화

조립한 그래프는 그림으로 확인할 수 있습니다. 디버깅에 매우 유용합니다.

PYTHON
# Jupyter 등에서
from IPython.display import Image, display
display(Image(graph.get_graph().draw_mermaid_png()))

이 장에서 배운 것

  • LangGraph는 작업 흐름을 그래프로 명시해 분기·반복·중단·복구를 깔끔하게 다루는 "공정도 엔진"이다.
  • 세 핵심 원시는 State(공유 데이터)·Node(일하는 함수)·Edge(전환)다.
  • 그래프는 정의 → 조립 → compile()invoke() 순으로 쓴다. 컴파일을 빼먹으면 안 된다.
  • 노드는 바뀐 키만 반환하고, 리듀서가 병합 방식(덮어쓰기 vs 누적)을 정한다.

✍️ 확인 문제

  1. StateGraph를 만들고 노드·엣지를 추가했는데 invoke가 안 된다. 무엇을 빠뜨렸을 가능성이 높은가?
  2. 노드 함수가 상태 전체가 아니라 "바뀐 키만" 반환하도록 설계된 이유는 무엇인가?
  3. 대화 기록을 담는 messages 키가 매 노드마다 덮어쓰여 사라진다. 어떻게 고치는가?
이전 부: 10. 핸드오프
다음 장: 12. 조건부 엣지와 도구 호출