18차시. State 이해
6. 실습 ② — Reducer 비교 실험
출처: 원본 PDF 18차시_State이해.pdf 13쪽 · 쪽 그림 13쪽
📝 붙였을 때 vs 뗐을 때
from typing import TypedDict, Annotated, List
import operator
from langgraph.graph import StateGraph, START, END
# ── 실험 A: Reducer 없음 ──
class StateA(TypedDict):
log: List[str]
# ── 실험 B: Reducer 있음 ──
class StateB(TypedDict):
log: Annotated[List[str], operator.add]
def step1(state): return {"log": ["1단계 완료"]}
def step2(state): return {"log": ["2단계 완료"]}
def step3(state): return {"log": ["3단계 완료"]}
def run(StateCls, label):
g = StateGraph(StateCls)
for name, fn in [("s1", step1), ("s2", step2), ("s3", step3)]:
g.add_node(name, fn)
g.add_edge(START, "s1")
g.add_edge("s1", "s2")
g.add_edge("s2", "s3")
g.add_edge("s3", END)
result = g.compile().invoke({"log": []})
print(f"{label}: {result['log']}")
run(StateA, "Reducer 없음")
run(StateB, "Reducer 있음")
실행 결과
──────────────────────────────────────────
Reducer 없음: ['3단계 완료']
Reducer 있음: ['1단계 완료', '2단계 완료', '3단계 완료']💡 직접 실행해보세요
이 차이를 글로 읽는 것과 눈으로 보는 것은 다릅니다.
나중에 "로그가 하나만 남는데?" 하고 당황할 때
오늘 본 이 화면이 떠오를 겁니다.
실습 코드 · 18/reducer_demo.py
실습 코드: 실습/src/18/reducer_demo.py · 위의 원본 PDF 18차시 13쪽 「6️⃣ 실습 ② — Reducer 비교 실험」과 이어집니다.
# ============================================================
# 18차시 | State 이해
# 교안: 18차시_State이해.pptx
# 역할: reducer 가 없을 때와 있을 때 log 리스트가 어떻게 다른지 비교한다.
# 입력: log (빈 리스트)
# 출력: reducer 없음이면 마지막 값만, 있으면 세 단계가 이어 붙은 리스트
#
# State = 공유 노트. 같은 노드 함수를 두 그래프에 그대로 넣는다.
# Node = step1, step2, step3. 각자 log 에 한 칸짜리 리스트를 돌려준다.
# Edge = START → step1 → step2 → step3 → END. 두 그래프의 연결은 같다.
# reducer = 노드가 돌려준 값을 기존 값에 합치는 규칙.
# 없으면 그 키는 통째로 덮어쓴다.
# Annotated[list[str], operator.add] 이면 리스트를 뒤에 이어 붙인다.
# (새 리스트로 교체하지 않는다.)
# compile / invoke = 각 그래프를 확정한 뒤 빈 log 로 한 번씩 실행한다.
#
# 흐름 (A, B 둘 다 같은 연결)
# START → step1 → step2 → step3 → END
#
# A log: [] → ["1단계 완료"] → ["2단계 완료"] → ["3단계 완료"]
# B log: [] → ["1단계 완료"]
# → ["1단계 완료", "2단계 완료"]
# → ["1단계 완료", "2단계 완료", "3단계 완료"]
# ============================================================
from typing import TypedDict, Annotated
import operator
from langgraph.graph import StateGraph, START, END
# ============================================================
# 1. Reducer가 없는 State
# ============================================================
class StateA(TypedDict):
"""log에 새 값이 들어오면 기존 값을 덮어씁니다."""
log: list[str]
# ============================================================
# 2. Reducer가 있는 State
# ============================================================
class StateB(TypedDict):
"""log에 새 값이 들어오면 기존 리스트 뒤에 추가합니다."""
log: Annotated[list[str], operator.add]
# ============================================================
# 3. Node 정의
# ============================================================
# 노드 step1
# 읽기: 없음 (state 는 받지만 사용하지 않음)
# 쓰기: log
def step1(state):
"""1단계 실행 기록을 반환합니다."""
return {"log": ["1단계 완료"]}
# 노드 step2
# 읽기: 없음
# 쓰기: log
def step2(state):
"""2단계 실행 기록을 반환합니다."""
return {"log": ["2단계 완료"]}
# 노드 step3
# 읽기: 없음
# 쓰기: log
def step3(state):
"""3단계 실행 기록을 반환합니다."""
return {"log": ["3단계 완료"]}
# ============================================================
# 4. 실험 A - Reducer 없음
# ============================================================
# StateA를 사용하는 그래프를 만듭니다.
graph_a = StateGraph(StateA)
# 노드를 등록합니다.
graph_a.add_node("step1", step1)
graph_a.add_node("step2", step2)
graph_a.add_node("step3", step3)
# START → step1 → step2 → step3 → END 순서로 연결합니다.
graph_a.add_edge(START, "step1")
graph_a.add_edge("step1", "step2")
graph_a.add_edge("step2", "step3")
graph_a.add_edge("step3", END)
# compile 후 invoke. reducer 가 없으면 각 return 이 log 를 덮어쓴다.
app_a = graph_a.compile()
# 빈 log에서 실행을 시작합니다.
result_a = app_a.invoke({"log": []})
# 최종 log를 출력합니다.
print("Reducer 없음:", result_a["log"])
# ============================================================
# 5. 실험 B - Reducer 있음
# ============================================================
# StateB를 사용하는 그래프를 만듭니다.
graph_b = StateGraph(StateB)
# 노드를 등록합니다.
graph_b.add_node("step1", step1)
graph_b.add_node("step2", step2)
graph_b.add_node("step3", step3)
# START → step1 → step2 → step3 → END 순서로 연결합니다.
graph_b.add_edge(START, "step1")
graph_b.add_edge("step1", "step2")
graph_b.add_edge("step2", "step3")
graph_b.add_edge("step3", END)
# 같은 노드라도 reducer 가 있으면 log 리스트를 뒤에 이어 붙인다.
app_b = graph_b.compile()
# 빈 log에서 실행을 시작합니다.
result_b = app_b.invoke({"log": []})
# 최종 log를 출력합니다.
print("Reducer 있음:", result_b["log"])실행 결과 샘플 (교안 기록. API 키가 필요한 코드는 키 없이 맞춘 예시이고, 모델의 답은 실행할 때마다 달라집니다)
===== 실행 결과 샘플 =====
실행: cd 실습/src/18
python reducer_demo.py
(실제 실행 출력)
Reducer 없음: ['3단계 완료']
Reducer 있음: ['1단계 완료', '2단계 완료', '3단계 완료']
경로: step1 → step2 → step3 → END코드 해설
reducer 가 없을 때와 있을 때 log 리스트가 어떻게 다른지 비교한다. 입력은 log (빈 리스트)이고, 출력은 reducer 없음이면 마지막 값만, 있으면 세 단계가 이어 붙은 리스트입니다.
주석에 적힌 흐름입니다.
흐름 (A, B 둘 다 같은 연결)
START → step1 → step2 → step3 → END
A log: [] → ["1단계 완료"] → ["2단계 완료"] → ["3단계 완료"]
B log: [] → ["1단계 완료"]
→ ["1단계 완료", "2단계 완료"]
→ ["1단계 완료", "2단계 완료", "3단계 완료"]코드 앞에서 고정해 두는 말입니다.
- 18차시 | State 이해
- 교안: 18차시_State이해.pptx
- State = 공유 노트. 같은 노드 함수를 두 그래프에 그대로 넣는다.
- Node = step1, step2, step3. 각자 log 에 한 칸짜리 리스트를 돌려준다.
- Edge = START → step1 → step2 → step3 → END. 두 그래프의 연결은 같다.
- reducer = 노드가 돌려준 값을 기존 값에 합치는 규칙.
- 없으면 그 키는 통째로 덮어쓴다.
- Annotated[list[str], operator.add] 이면 리스트를 뒤에 이어 붙인다.
함수와 클래스로 나누어 읽습니다.
StateA: log에 새 값이 들어오면 기존 값을 덮어씁니다.StateB: log에 새 값이 들어오면 기존 리스트 뒤에 추가합니다.step1: 노드 step1 읽기: 없음 (state 는 받지만 사용하지 않음) 쓰기: logstep2: 노드 step2 읽기: 없음 쓰기: logstep3: 노드 step3 읽기: 없음 쓰기: log
파일 맨 아래 실행 결과 샘플 주석은 실행되지 않습니다. 화면 출력은 다음 절에 다시 적어 두었습니다.