ADR-004: 요소 단위 API — Run / Paragraph / Cell / Section
맥락
v3 의 ADR-003 으로 다중 문서가 깔끔히 정착했고, 각 Document 에 7개 컬렉션 (fields, bookmarks, hyperlinks, images, paragraphs, styles, tables) 이 자리잡았 습니다. 그러나 컬렉션이 반환하는 개별 element 들은 여전히 빈약합니다:
doc.paragraphs[0]→Paragraph— 현재text,index정도만doc.tables[0]→Table—cell(r, c)정도만doc.tables[0].cell(1, 1)→Cell—text,border,fillsetter 가 약속만 있음 (migration v1→v2 가이드 참조)Run(글자 단위 형식 단위) 클래스 자체가 없음
결과적으로 사용자가 “이 단락의 정렬을 바꾸고 글자 색만 바꾸고 싶다” 같은 작업을 할 때 여전히 doc.actions.X.run() raw 로 내려가야 합니다. v3 의 doc.X 일관성이 element 레벨에서 깨지는 셈입니다.
xlwings 와 비교:
# xlwings — element 까지 풀 API
sheet.range("A1:B5").value = [[1,2],[3,4]]
sheet.range("A1").color = (255, 200, 200)
sheet.range("A1").font.bold = Truehwpapi 가 도달해야 할 비슷한 수준:
# 목표 (v3.x — 이 ADR)
para = doc.paragraphs[0]
para.text = "새 본문"
para.parashape.align = "center"
for run in para.runs:
run.charshape.bold = True
cell = doc.tables[0].cell(1, 1)
cell.text = "Q1"
cell.fill = "#FFE0E0"
cell.border = {"top": "double", "bottom": "solid"}결정
4 종 element 클래스를 정식 정의 + 각자 자기 책임 범위 명확화. 모든 element 는 ADR-003 의 doc-activate 패턴을 그대로 따름 — element 의 메소드 진입 시 owning doc 자동 활성화.
Element 매트릭스
| Element | 컬렉션 진입 | 책임 |
|---|---|---|
Paragraph |
doc.paragraphs[i] |
단락 텍스트, ParaShape, runs 컬렉션 |
Run |
paragraph.runs[i] |
한 글자형식 구간의 text, CharShape |
Table |
doc.tables[i] |
rows/cols, cell(r,c), 표 단위 BorderFill |
Cell |
table.cell(r, c) |
text, fill, border, charshape, parashape |
Section (보류) |
doc.sections[i] |
페이지 설정, 머리말/꼬리말 (v3.1) |
표면 (각 element 의 핵심 surface)
class Paragraph:
index # 단락 번호 (0-based)
text # r/w property
parashape # ParaShape (r/w)
charshape # 단락 시작 글자의 CharShape (r/w convenience)
runs # RunCollection
insert_text(s) # 이 단락 끝에 추가
delete() # 이 단락 제거
activate() # 커서를 이 단락으로
class Run:
paragraph # owning Paragraph
text # r/w
charshape # CharShape (r/w)
start, end # 단락 안의 글자 위치 범위
class Table:
index, rows, cols
cell(r, c) → Cell
iter_cells() # row-major
delete()
border_fill # 표 전체 BorderFill
activate()
class Cell:
table # owning Table
row, col, address (e.g. "B2")
text # r/w
charshape / parashape # r/w
fill # str | Color | None — 셀 배경
border # dict 형 setter ({"top": "solid", ...})
width / height # r/w (HWPUNIT)
activate() # 이 셀로 커서 이동
doc-activate 패턴 적용
모든 element 는 owning doc 의 reference 를 갖고, 자기 메소드/setter 진입 시 doc.activate() 를 호출합니다 (ADR-003 의 _DocActions 와 동일 패턴):
class Paragraph:
def __init__(self, doc, index):
self._doc = doc
self._index = index
@property
def text(self):
self._doc.activate()
# ... GetTextFile + 범위 추출
return ...
@text.setter
def text(self, value):
self._doc.activate()
# ... select range + insert_text다중 문서 환경에서 for p in doc1.paragraphs: p.text 와 for p in doc2.paragraphs: p.text 가 자동 doc-switching 되면서 안전하게 동작.
CharShape / ParaShape — 기존 ParameterSet 재사용
v2 에서 이미 hwpapi.low.parametersets.CharShape / ParaShape 가 있음. element 의 .charshape / .parashape 는 이들의 인스턴스를 반환/할당. 사용자는 익숙한 dict-like 접근:
cs = run.charshape # CharShape 인스턴스
cs.bold = True
cs.height = 1400
run.charshape = cs # setter — 적용
# 또는 dict 한방
run.charshape = {"bold": True, "height": 1400}사용자 멘탈 모델
App (process)
└── Document (per-doc)
├── 컬렉션 (fields, bookmarks, ...)
├── paragraphs : Collection[Paragraph]
│ └── runs : Collection[Run]
└── tables : Collection[Table]
└── cell(r, c) : Cell
각 레이어는 자기 책임만 — App 은 lifecycle, Document 는 doc-scoped ops, Element 는 element-scoped ops.
마이그레이션 단계
Phase 1 — Paragraph + Run
Paragraph.textgetter/setterParagraph.parashapegetter/setterParagraph.runs(RunCollection)Run.text,Run.charshapegetter/setter- 테스트:
tests/test_paragraph.py,tests/test_run.py
Phase 2 — Table + Cell
Table.cell(r, c)가Cell반환Cell.text,Cell.fill,Cell.border(border dict 형)Cell.charshape,Cell.parashapeCell.activate()가 커서를 셀로- 테스트:
tests/test_table.py,tests/test_cell.py
Phase 3 — RunCollection + iter_cells
paragraph.runs의 iteration / 인덱싱Table.iter_cells()row-major + filter- 테스트 추가
Phase 4 — Section (보류)
- 페이지 설정, 머리말/꼬리말 — v3.1 또는 별도 ADR
사용 예
단락 / Run
doc = app.docs.open("report.hwp")
# 1단락 텍스트와 정렬
para = doc.paragraphs[0]
para.text = "1분기 보고"
para.parashape = {"align": "center", "line_spacing": 180}
# 안에 있는 모든 run 을 굵게
for run in para.runs:
run.charshape = {"bold": True}
# 또는 한 단락 안의 일부만
para.runs[0].charshape = {"text_color": "#E74C3C"}Table / Cell
doc.insert_table(rows=4, cols=4)
table = doc.tables[-1] # 방금 만든 표
table.cell(0, 0).text = "제품"
table.cell(0, 1).text = "Q1"
table.cell(0, 2).text = "Q2"
table.cell(0, 3).text = "Q3"
# 헤더 셀 일괄 서식
for c in range(4):
cell = table.cell(0, c)
cell.fill = "#34495E"
cell.charshape = {"bold": True, "text_color": "#FFFFFF"}
# 첫 열만 굵은 테두리
for r in range(1, 4):
table.cell(r, 0).border = {"right": "double"}다중 문서 + element
src = app.docs.open("source.hwp")
dst = app.docs.add()
# doc-activate 자동 — element 가 owning doc 으로 자동 전환
for para in src.paragraphs:
if para.text.startswith("□"):
# 이 시점엔 src 활성, src.paragraphs[i].text 는 정상
pass
# dst 작업 — 자동 전환
for row in [["A", "B"], ["C", "D"]]:
dst.insert_table(rows=1, cols=2)
for c, val in enumerate(row):
dst.tables[-1].cell(0, c).text = val결과
긍정
- API 일관성 —
doc.X,paragraph.X,cell.X모두 같은 패턴 - 다중 문서 안전 — element 도 doc-activate 자동
raw action의존도 감소 — Cell 서식 등 일상 작업이 element 메소드로 가능- xlwings/openpyxl 사용자 친숙 — sheet.range, cell.font 등과 동형
- 테스트 가능 — Mock 으로 element 단위 검증
부정 / 위험
- 클래스 수 증가 — Paragraph/Run/Table/Cell 4개 신규. 컬렉션 포함 8개 (RunCollection, CellCollection 도) 이미 늘어남.
- HWP 의 element 인덱싱 불안정 — 문서 편집 중 paragraph index 가 밀릴 수 있음. cell 도 표 행/열 추가 시 변동. 안정적 식별자가 필요한 경우 사용자가 reload 해야 함 (xlwings 도 동일).
- CharShape/ParaShape 의 process-wide pset 문제 — element 마다 자기 pset 을 보유하지 않고 process 레벨 공유. doc-activate 와 결합해 안전하나, 동시성은 여전히 보장 안 됨 (Python GIL 가정).
- Phase 1/2 구현 비용 — 각 element 에 대해 텍스트 범위 추적, selection 관리, charshape 적용/읽기 인프라 필요. 약 600~1000 LOC 추정.
미해결 질문
- Run 의 경계 정의 — HWP 에서 “동일 charshape 의 연속 글자” 가 run 의 자연스러운 정의이지만, 단락 안에서 이를 효율적으로 순회하는 API 가 있는지 검증 필요. 없으면
paragraph.text를 캐릭터 단위로 스캔하며 charshape 비교로 구간 결정 (느림). - Cell.border 의 dict 형 설계 —
{"top": "solid"}가 단일 변,{"all": "solid"}가 4변 일괄,{"outer": "solid", "inner": "dot"}같은 alias 까지 지원할지. - Element 의 동일성 (
==) — 같은 표의 같은 셀을 두 번 가져온Cell두 개가==인가? row/col 비교? raw COM 핸들 비교? - Section / Header / Footer — 이번 ADR 범위에 포함 여부. 분리 (v3.1) 권장.
- iter 방식의 안정성 —
for p in doc.paragraphs:중에 단락이 추가/삭제되면? generator-based 으로 lazy 하게 갈지, snapshot 으로 고정할지.
참고
- ADR-003: 다중 문서 재설계 — 이 ADR 의 토대 (doc-activate 패턴, _DocActions proxy)
- v2→v3 마이그레이션 가이드
- xlwings Range/Cell API
- openpyxl Cell API