Step 334. 포트폴리오 통합 — 실력의 증명 패키지: 흩어진 증거를 하나의 이야기로
Level 4 — 전문가 | 난이도 ★★★☆☆ | 예상 소요 시간 2일 (자산 인벤토리 반나절 + 대표작 선정·통합 페이지 1일 + 이력서 반나절)
전제: Step 294의 블로그 포트폴리오 v1, Step 321~323의 CVE 보고서·오픈소스 공개, Step 332~333의 발표 자료, 그리고 지금까지의 대회 기록 전부. 이 챕터는 새로 만드는 것이 아니라 모으는 프로젝트 챕터입니다.
- 준비물: Write-up이 있는 블로그, GitHub 계정, 대회 기록(CTFtime 포함), 자격증 목록, 파이썬 실행 환경. 인벤토리 스크립트는 여러분의 폴더에서 직접 돌리는 실측 도구이고, 통합 페이지의 완성 장면은 화면 예시입니다.
- 주의: 포트폴리오는 공개 문서입니다 — 실명·소속·개인 연락처는 가공하고, 모든 수치는 제3자가 확인 가능한 것만 씁니다(Step 294의 규칙).
- ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다. 포트폴리오에 싣는 사례는 퇴출된 머신, 종료된 대회, 공개 허가된 분석만 사용합니다.
지금까지의 348개 스텝은 증거로 가득합니다. 300개가 넘는 완료 과제, 수십 편의 Write-up, CVE 분석 보고서, 오픈소스 저장소, 대회 기록, 자격증, 발표 자료까지. 그런데 이 증거들은 블로그와 GitHub와 CTFtime과 하드디스크에 흩어져 있습니다. 채용 담당자도, 협업을 제안하려는 연구자도, 이것을 대신 모아 주지 않습니다.
오늘의 작업은 그 흩어진 증거를 한 페이지의 이야기로 만드는 것입니다. "나는 이런 문제를 풀어 온 사람이다"라는 문장 하나에 링크와 숫자를 붙이는 일 — 개인 브랜딩이라고 부르지만, 실체는 정리와 검증 가능성입니다. Step 294가 블로그라는 한 축의 정돈이었다면, 오늘은 전 채널의 통합입니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 흩어진 자산(글·보고서·저장소·대회·자격·발표)을 스크립트로 인벤토리화한다
- 양이 아니라 깊이 기준으로 대표작 5~7개를 선정하고 4줄 요약을 붙인다
- 통합 포트폴리오 페이지(About 또는 GitHub 프로필 README)를 완성한다
- GitHub 프로필을 정돈한다 — pinned 저장소와 지속적인 커밋 기록
- 같은 내용을 1장 A4 이력서로 변환한다 — "아는 것"이 아니라 "한 것" 중심으로
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | 파이썬 3(인벤토리 스크립트), 마크다운(통합 페이지·이력서), GitHub 프로필 설정 |
| 오늘의 명령 | python step334_inventory.py <자산 폴더> — 자산 집계 + 통합 페이지 초안 생성 |
| 필요한 개념 | 인벤토리화, 대표작 선정 기준, 4줄 요약(문제/접근/결과/배운 것), 검증 가능성 |
| 오늘의 산출물 | 통합 포트폴리오 페이지(대표작 요약 포함) + 1장 이력서 + 정돈된 GitHub 프로필 |
2-1. 인벤토리 — 모으기 전에 세어본다
포트폴리오 작업의 첫 순서는 글쓰기가 아니라 재고 조사입니다. 사람은 자기가 쌓은 것의 양을 과소평가합니다 — 매일의 과제로 겪은 것들은 "당연한 것"으로 느껴지기 때문입니다. 그래서 기억이 아니라 파일 시스템으로 셉니다.
인벤토리의 대상은 일곱 종류입니다 — Write-up, CVE·취약점 분석 보고서, 오픈소스 저장소, 대회 기록(순위 포함), 자격증, 발표 자료, 멘토링 경력. 이 중 다섯 종류는 파일과 공개 기록으로 기계가 셀 수 있고, 오늘의 스크립트가 그 부분을 담당합니다.
2-2. 대표작 — 양이 아니라 깊이
인벤토리가 끝나면 유혹이 옵니다 — 전부 나열하고 싶어집니다. 그것이 포트폴리오 실패의 첫 번째 패턴입니다. 읽는 사람의 시간은 유한하고, 50개의 링크는 0개의 링크와 같은 효과를 냅니다 — 아무것도 클릭되지 않습니다.
대표작은 5~7개, 기준은 Step 294에서 세운 네 가지를 계승합니다.
| 기준 | 확인 질문 |
|---|---|
| 난이도 | 당시의 나를 밀어붙인 문제인가 |
| 서사 | 시행착오와 버린 가설이 드러나는가 |
| 재현성 | 독자가 명령과 출력만으로 따라 할 수 있는가 |
| 분야 균형 | 한 분야에 몰리지 않는가 (단, 지원 직무가 정해졌다면 그 분야를 앞에) |
Level 4에서 추가되는 기준이 하나 있습니다 — 다양성의 축을 넓히는 것. Write-up만 7개가 아니라, Write-up + CVE 보고서 + 오픈소스 도구 + 대회 기록 + 발표 자료가 섞여야 "풀이자"가 아니라 "만들고 분석하고 가르치는 사람"의 형상이 완성됩니다.
2-3. 4줄 요약 — 대표작의 포장 규격
선정한 대표작마다 붙이는 요약의 포맷은 네 줄로 고정합니다.
문제: 무엇을 풀어야 했는가 (한 문장)
내 접근: 처음 시도와 그것이 바뀐 지점
결과: 숫자 — 점수, 순위, 기간, 재현 여부
배운 것: 다음의 나에게 남은 것 (기법이 아니라 판단 기준)
네 줄인 이유는 읽는 사람 때문입니다. 채용 담당자는 대표작 링크를 눌러 글 전체를 읽지 않고, 링크 옆의 요약을 먼저 봅니다. 네 줄이 그 사람의 30초 안에 들어가고, 그 안에 "문제-사고-결과-성장"의 회로가 완성되어야 합니다. "배운 것" 칸이 비어 있는 요약은 아직 완성이 아닙니다 — 그 칸이 이 사람이 같은 문제를 다시 만났을 때 더 빨라진다는 증거입니다.
2-4. 이력서 변환 — 같은 내용, 다른 문법
통합 페이지와 이력서는 내용이 같고 문법이 다릅니다. 페이지는 탐색하는 독자를 위한 것이고, 이력서는 1장 A4 안에 스캔하는 독자를 위한 것입니다.
문법의 차이는 한 가지로 수렴합니다 — 기술을 "아는 것"이 아니라 "그걸로 한 것"으로 쓰는 것. "Python, 네트워크 프로토콜, 리버싱 가능"은 아는 것의 목록입니다. "퍼징 캠페인 자동화 스크립트로 취약점 2건 발굴(CVE-XXXX-XXXX)", "국내 메이저 CTF 본선 5위(40팀)"는 한 것의 목록입니다. 같은 실력이어도 후자만이 검증 가능하고, 검증 가능한 문장만이 면접으로 연결됩니다.
3. 따라 하기
3-1. 자산 폴더 구성 — 증거를 한곳에 모은다
실제 자산은 블로그·GitHub·CTFtime에 흩어져 있지만, 집계를 위해 로컬에 거울 폴더를 만듭니다. 링크와 파일명만 모으는 것이니 복사 부담은 작습니다.
portfolio/
├── posts/ ← Write-up 마크다운 (블로그 소스 그대로)
├── reports/ ← CVE·취약점 분석 보고서
├── repos/ ← 오픈소스 저장소 (폴더 이름만 — 저장소별 빈 폴더)
├── talks/ ← 발표 자료
└── records.csv ← 대회 기록: 대회명,순위,참가팀수,비고
records.csv는 직접 만듭니다 — CTFtime 팀 페이지와 대회 공식 스코어보드를 보며 옮깁니다. 이 CSV가 대회 기록의 단일 원천이 됩니다. 이력서에도, 통합 페이지에도, 다음 대회 신청서에도 이 파일의 숫자만 쓰는 규칙을 세우면 문서마다 숫자가 다른 사고를 막을 수 있습니다.
3-2. 인벤토리 스크립트 — 기계가 먼저 센다
거울 폴더를 스캔해 자산을 집계하고 통합 페이지의 초안까지 생성하는 스크립트입니다. step334_inventory.py로 저장하세요.
# step334_inventory.py — 포트폴리오 자산 인벤토리 + 통합 페이지 스켈레톤 생성기
# 사용: python step334_inventory.py <포트폴리오 자산 폴더>
import csv
import re
import sys
from pathlib import Path
def count_md(folder: Path):
if not folder.is_dir():
return []
return sorted(p.name for p in folder.glob("*.md"))
def read_category(path: Path):
m = re.search(r"^category:s*(.+)$", path.read_text(encoding="utf-8"), re.M)
return m.group(1).strip() if m else "(미분류)"
def main():
root = Path(sys.argv[1])
posts = count_md(root / "posts")
reports = count_md(root / "reports")
repos = sorted(p.name for p in (root / "repos").iterdir() if p.is_dir())
if (root / "repos").is_dir() else []
talks = count_md(root / "talks")
cats = {}
for name in posts:
c = read_category(root / "posts" / name)
cats[c] = cats.get(c, 0) + 1
records = []
rec = root / "records.csv"
if rec.exists():
with rec.open(encoding="utf-8") as f:
records = list(csv.DictReader(f))
print("=== 포트폴리오 자산 인벤토리 ===")
print(f"Write-up: {len(posts)}편 | 분석 보고서: {len(reports)}건"
f" | 오픈소스 저장소: {len(repos)}개 | 발표 자료: {len(talks)}건")
print("n[Write-up 분야 분포]")
for c, n in sorted(cats.items(), key=lambda x: -x[1]):
print(f" {c}: {n}편")
print("n[대회 기록]")
for r in records:
rank, total = r["순위"], r["참가팀수"]
pct = f"상위 {int(rank)/int(total)*100:.0f}%"
if rank.isdigit() and total.isdigit() else "기록 없음"
print(f" {r['대회명']}: {rank}위 / {total}팀 ({pct}) — {r['비고']}")
skel = root / "portfolio_draft.md"
lines = [
"# 통합 포트폴리오 (초안 — 스크립트 생성)",
"",
"한 줄 소개: (여기에 — 어떤 문제를 풀어 온 사람인지 한 문장)",
"",
"### 대표작 (5~7개를 손으로 선정 — 문제/접근/결과/배운 것 4줄씩)",
"1. (posts/ 에서 선정) — ",
"2. ",
"",
"### 정량 기록 (스크립트가 채운 부분 — 검증 가능한 것만 유지)",
f"- Write-up 누적 {len(posts)}편 (분야: "
+ ", ".join(f"{c} {n}" for c, n in sorted(cats.items(), key=lambda x: -x[1])) + ")",
f"- 취약점 분석 보고서 {len(reports)}건 / 오픈소스 저장소 {len(repos)}개 / 발표 {len(talks)}건",
"- 대회: " + "; ".join(f"{r['대회명']} {r['순위']}/{r['참가팀수']}" for r in records),
"",
"### 연락처: (대회용 계정 — 실명·개인 이메일 금지)",
]
skel.write_text("n".join(lines), encoding="utf-8")
print(f"n통합 페이지 초안 생성: {skel.name}"
" — 대표작 선정과 4줄 요약은 사람이 채웁니다.")
if __name__ == "__main__":
main()
샘플 거울 폴더(Write-up 8편, 보고서 2건, 저장소 3개, 발표 1건, 대회 기록 3건)에 대고 실행한 실측 출력입니다:
=== 포트폴리오 자산 인벤토리 ===
Write-up: 8편 | 분석 보고서: 2건 | 오픈소스 저장소: 3개 | 발표 자료: 1건
[Write-up 분야 분포]
pwn: 7편
web: 1편
[대회 기록]
예선통과컵: 12위 / 180팀 (상위 7%) — 예선 통과
본선메이저: 5위 / 40팀 (상위 12%) — 본선 5위
국제온라인: —위 / —팀 (기록 없음) — 완주
통합 페이지 초안 생성: portfolio_draft.md — 대표작 선정과 4줄 요약은 사람이 채웁니다.
읽는 법: 세 가지를 봅니다. ① 총량의 확인 — "낼 만한 게 부족한 것 같다"는 불안이 숫자로 정정됩니다. 샘플은 8편뿐이지만, 여러분의 실제 폴더에서는 수십 편이 나올 것입니다. 부족한 게 아니라 정리가 안 된 것이었다는 사실이 여기서 증명됩니다. ② 분야 분포의 편향 — 샘플은 pwn 7편, web 1편입니다. 이 편향이 대표작 선정(3-3)의 입력값이 됩니다. ③ "기록 없음" 행 — 국제온라인 대회는 순위를 안 적고 완주만 적었습니다. 확인 불가능한 수치를 비워 두는 것은 결함이 아니라 규칙 준수입니다.
생성된 portfolio_draft.md의 내용도 실측 그대로입니다:
# 통합 포트폴리오 (초안 — 스크립트 생성)
한 줄 소개: (여기에 — 어떤 문제를 풀어 온 사람인지 한 문장)
### 대표작 (5~7개를 손으로 선정 — 문제/접근/결과/배운 것 4줄씩)
1. (posts/ 에서 선정) —
2.
### 정량 기록 (스크립트가 채운 부분 — 검증 가능한 것만 유지)
- Write-up 누적 8편 (분야: pwn 7, web 1)
- 취약점 분석 보고서 2건 / 오픈소스 저장소 3개 / 발표 1건
- 대회: 예선통과컵 12/180; 본선메이저 5/40; 국제온라인 —/—
### 연락처: (대회용 계정 — 실명·개인 이메일 금지)
기계가 채운 곳과 빈칸의 경계를 보세요 — 정량 기록은 스크립트가, 대표작과 한 줄 소개는 사람이 채웁니다. 이 분업이 2-1~2-3의 원칙 그대로입니다.
3-3. 대표작 선정과 4줄 요약
2-2의 기준표로 5~7개를 고릅니다. 샘플 폴더 기준의 선정 화면 예시입니다:
■ 대표작 선정 결과 (화면 예시)
1. [Write-up] heap-house — 힙 하우스 계열 기법을 처음으로 혼자 재현한 기록
문제: 최신 glibc의 힙 보호 기법이 적용된 바이너리 공격
내 접근: house of X 계열을 순서대로 시도, 3번째에서 검사 로직 발견
결과: 6시간 만에 셸 획득, 대회 내 2솔브
배운 것: 보호 기법 목록을 먼저 열거하는 것이 시도 순서를 결정한다
2. [보고서] CVE-2026-2222 독립 분석 — 패치 diff로 취약점을 역추적
문제: 공개된 보안 패치에서 취약점의 원인을 독립적으로 특정
내 접근: diff의 변경 함수 3개를 후보로 두고 PoC로 하나씩 소거
결과: 근본 원인 특정 + 재현 PoC 작성, 보고서 12페이지
배운 것: 패치는 "답이 적힌 문제"다 — diff를 읽는 눈이 연구의 출발점
3. [오픈소스] payload-fuzzer — 대회용 페이로드 생성기를 공개 저장소로
... (4줄 생략 예시)
선정할 때 3-2 출력의 분야 편향을 되돌아보세요. 샘플처럼 pwn에 몰렸다면, web의 1편이 자동으로 후보가 됩니다 — 대표작은 "가장 잘한 것"의 목록인 동시에 "내가 커버하는 범위"의 광고이기 때문입니다.
3-4. 통합 페이지 배치 — About 또는 GitHub 프로필 README
완성한 초안을 배치할 곳은 둘 중 하나입니다. 블로그에 이미 독자가 있다면 About 페이지, 채용 열람이 주목적이라면 GitHub 프로필 README(계정 이름과 같은 이름의 저장소에 넣는 특수 README)입니다. 둘 다 해도 되지만, 내용은 하나의 원천에서 관리하세요 — 지금 만든 portfolio_draft.md가 그 원천입니다.
GitHub 프로필 정돈의 체크리스트는 세 가지입니다.
[ ] pinned 저장소 6개 선정 — 대표 도구 + Write-up 모음 + CVE 분석 저장소
(트로피가 아니라 증거를 고정한다: README가 성의 있는 저장소만)
[ ] 각 pinned 저장소 README 점검 — 첫 화면에 "무엇을/왜/어떻게 쓰는가" 3줄
[ ] 커밋 잔디의 지속성 — 일일 커밋이 목표가 아니라 "끊기지 않는 것"이 목표
잔디에 대해 한 가지만 짚습니다. 채용 담당자가 잔디에서 보는 것은 밀도가 아니라 지속입니다 — 오랜 기간에 걸친 드문드문한 초록이, 두 달 빽빽하고 넉 달 공백인 패턴보다 신뢰를 줍니다. 잔디를 꾸미려는 시도(자동 커밋 봇 등)는 확대경이 들어오는 순간 역효과입니다.
3-5. 1장 이력서 — 같은 증거의 압축
통합 페이지가 완성되면 그것을 1장 A4로 압축합니다. 압축의 규칙은 2-4의 문법 하나입니다 — "아는 것"을 "한 것"으로.
■ 이력서 기술란 변환 예시 (화면 예시)
[변환 전 — 아는 것]
Python, C, 네트워크 프로토콜 이해, 웹 취약점, 리버싱, CTF 경험
[변환 후 — 한 것]
- CTF: 국내 메이저 대회 본선 5위(40팀), Write-up 누적 50편+
- 취약점 분석: 공개 패치 diff 독립 분석 2건, 재현 PoC 포함 보고서 작성
- 오픈소스: 대회용 익스플로잇 라이브러리 등 저장소 3개 운영 (README·테스트 포함)
- 발표·멘토링: 입문자 대상 힙 입문 세미나 1회, 스터디 멘토링 N주
읽는 법: 변환 전의 모든 단어가 변환 후에도 살아 있지만, 전부 동사의 목적어로 이동했습니다. "리버싱"은 사라지고 "패치 diff 독립 분석"이 남았습니다. 채용 화면에서 읽히는 것은 후자뿐이라는 것을 기억하세요. 그리고 분량 규칙 — 1장을 넘기는 순간 가장 강한 문장이 묻힙니다. 빼는 것이 이력서 작업의 본질입니다.
4. 미션과 연습문제
미션 — 통합 포트폴리오 + 1장 이력서
- 3-1의 구조로 자산 거울 폴더를 만들고,
records.csv에 확인 가능한 대회 기록만 옮깁니다. step334_inventory.py를 실행해 자산 총량과 분야 분포를 확인하고, 생성된portfolio_draft.md를 받습니다.- 2-2의 기준표로 대표작 5~7개를 선정하고 각각 4줄 요약을 붙여 초안을 완성합니다 — 분야 편향이 보이면 균형을 조정합니다.
- 통합 페이지를 배치합니다(블로그 About 또는 GitHub 프로필 README) — 개인정보 가공과 수치 검증 가능성을 최종 점검합니다.
- GitHub pinned 저장소 6개를 정돈하고, 같은 내용을 2-4의 문법으로 1장 이력서에 압축합니다.
연습문제
문제 1. 포트폴리오 작업이 "모으기"가 아니라 "재고 조사"로 시작해야 하는 이유를, 사람이 자기 축적물을 과소평가하는 심리와 연결해 설명해 보세요.
문제 2. 대표작 목록에 Write-up만 7개를 나열하는 것이 왜 불완전한가요? Level 4에서 추가되는 선정 기준의 관점에서 답해 보세요.
문제 3. 4줄 요약의 "배운 것" 칸이 비어 있으면 그 요약이 증명하지 못하는 것은 무엇인가요?
문제 4. 이력서에서 "아는 것" 목록이 "한 것" 목록보다 약한 이유를, 검증 가능성과 면접으로의 연결 관점에서 설명해 보세요.
5. 모범 답안과 완료 기준
미션 모범 답안
검증 기준으로 확인하세요.
- 거울 폴더의 완결성: 일곱 종류의 자산 중 파일로 표현 가능한 것이 빠짐없이 모였는가 — 멘토링 경력처럼 파일이 없는 것은
records.csv의 비고나 별도 메모로 존재하는가. - 스크립트 실행: 인벤토리 출력의 총량·분포·대회 기록이 실제 자산과 일치하는가.
- 대표작의 품질: 5~7개 각각에 4줄 요약이 있고, "배운 것" 칸이 비어 있는 요약이 없는가 — 축의 다양성(글/보고서/도구/대회/발표)이 확보됐는가.
- 배치의 완성: 통합 페이지가 공개 상태이고, 모든 수치가 클릭 한두 번으로 제3자가 확인 가능한가.
- 이력서의 문법: 기술 목록이 전부 "한 것" 문장으로 변환됐고, 1장을 넘지 않는가.
연습문제 해답
문제 1 해답. 인간의 기억은 반복된 경험을 "당연한 것"으로 압축합니다 — 매일 한 과제는 축적물이 아니라 일상으로 느껴집니다. 그래서 머리로 떠올리면 실제보다 적게 느껴지고, 그 과소평가가 "낼 만한 게 부족하다"는 불안으로 나타납니다. 재고 조사는 이 왜곡을 파일 시스템이라는 객관 장치로 교정하는 절차입니다. 스크립트가 세어 준 숫자는 감정이 아니라 사실이고, 그 사실 위에서만 선정과 배치라는 판단이 정확해집니다. 순서가 뒤집히면 — 글부터 쓰면 — 없는 줄 알고 새로 만들거나, 있는 줄 알고 빠뜨리는 사고가 납니다.
문제 2 해답. Write-up만의 목록은 "문제 풀이자"라는 단일 형상만 증명하기 때문입니다. Level 4의 대표작은 축의 다양성을 요구합니다 — 풀이(Write-up), 분석(CVE 보고서), 제작(오픈소스 도구), 경쟁(대회 기록), 전달(발표·멘토링). 채용 담당자나 협업 제안자는 이 다섯 축의 조합으로 지원자의 역할 가능성을 판단합니다. 한 축만 있는 포트폴리오는 그 축이 아무리 깊어도 "다른 것도 할 수 있는가"라는 질문에 답을 주지 못합니다. 대표작 선정은 실력의 심사인 동시에 역할의 설계입니다.
문제 3 해답. "다음에 더 잘할 사람인가"를 증명하지 못합니다. 문제-접근-결과의 세 줄은 과거의 사실만 담고, 배운 것의 한 줄만이 그 경험이 판단 기준으로 전환됐음을 보여 줍니다. 읽는 사람의 관점에서 세 줄짜리 요약은 "그 문제를 풀었다"이고, 네 줄짜리 요약은 "그 문제가 이 사람을 바꿨다"입니다. 채용이 과거의 실적 심사인 동시에 미래의 성장 가능성 평가라는 점에서, 네 번째 줄이 요약의 절반의 가치를 가집니다.
문제 4 해답. "아는 것"은 주장이고 "한 것"은 증거이기 때문입니다. "리버싱 가능"은 지원자 스스로 검증할 수 없고 면접관도 서류에서 확인할 방법이 없는 문장입니다 — 면접에서 다시 물어봐야 하므로 서류 단계에서 아무 점수도 만들지 못합니다. 반면 "패치 diff 독립 분석 2건, PoC 포함 보고서"는 링크로 즉시 검증되고, 면접 질문을 "할 수 있나요"가 아니라 "어떻게 하셨나요"로 바꿉니다. 후자의 면접은 지원자가 가장 잘 아는 이야기를 하는 자리이고, 전자는 검증 시험대입니다. 같은 실력이라도 문법이 자리의 성격을 결정합니다.
완료 기준 체크리스트
- [ ] 자산 거울 폴더(posts/reports/repos/talks/records.csv)를 구성했다
- [ ]
step334_inventory.py를 실행해 총량·분야 분포·대회 기록을 확인했다 - [ ] 대표작 5~7개를 기준표로 선정하고 4줄 요약(특히 "배운 것")을 완성했다
- [ ] 통합 페이지를 배치했다 — 수치 검증 가능성과 개인정보 가공을 점검했다
- [ ] GitHub pinned 저장소를 증거 중심으로 정돈했다 (README 3줄 포함)
- [ ] 1장 이력서를 "한 것" 문법으로 작성했다
- [ ]
records.csv를 대회 기록의 단일 원천으로 정하고 모든 문서의 숫자를 통일했다
6. 흔한 실수와 해결
벽 1. 스크립트를 실행하니 폴더 인자 에러가 나요
증상:
root = Path(sys.argv[1])
~~~~~~~~^^^
IndexError: list index out of range
원인: 자산 폴더 경로를 인자로 안 줬습니다 (실측 — 인자 없이 실행하면 동일 에러).
해결: python step334_inventory.py portfolio처럼 폴더를 붙여 실행하세요. 경로에 공백이 있으면 큰따옴표로 감쌉니다. 하위 폴더(posts 등)가 없어도 스크립트는 죽지 않고 0건으로 세니, 일부만 있는 상태로 시작해도 됩니다.
벽 2. 대회 기록이 "기록 없음"으로 나와요
증상: records.csv에 적은 순위가 "(기록 없음)"으로 처리됩니다.
원인: 스크립트는 순위·참가팀수 칸이 둘 다 숫자일 때만 백분위를 계산합니다. "12등", "5위/40팀"처럼 문자가 섞이면 숫자로 인식하지 못합니다 (실측 — 샘플의 국제온라인 행이 이 경우).
해결: CSV의 순위·참가팀수 칸에는 숫자만 넣으세요 — 12, 40. "위", "팀" 같은 단위는 출력에서 스크립트가 붙입니다. 순위를 모르는 대회(완주만 기록)는 예시처럼 —를 넣는 것이 정답입니다.
벽 3. 대표작을 고르다 보니 자꾸 10개가 넘어가요
증상: 이것도 저것도 대표라서 7개로 못 줄입니다.
원인: 선정 기준이 아니라 애착으로 고르고 있는 상태입니다 — 오래 고생한 것이 대표작이라는 착각.
해결: 절차를 뒤집으세요 — 먼저 7칸짜리 빈 표를 만들고, 축별(풀이/분석/제작/경쟁/전달)로 한 칸씩만 채웁니다. 같은 축에서 경쟁하는 후보가 둘이면 "독자가 30초 안에 가치를 알 수 있는가"로 결판합니다 — 고생의 양은 독자에게 전달되지 않습니다. 빠진 것은 삭제가 아니라 통합 페이지 본문의 전체 목록 링크에 남습니다. 대표작은 커튼이고, 목록은 무대입니다.
벽 4. 통합 페이지에 뭘 썼는지 이력서와 숫자가 달라요
증상: 블로그에는 "Write-up 50편", 이력서에는 "45편", GitHub에는 "40여 편".
원인: 각 문서를 다른 날에 따로 고쳤기 때문입니다 — 원천이 없으면 문서는 반드시 갈라집니다.
해결: 3-1의 규칙으로 돌아가세요 — 숫자의 단일 원천(records.csv와 인벤토리 스크립트 출력)을 정하고, 모든 문서는 그 출력을 복사해서 씁니다. 숫자가 바뀔 때는 원천을 먼저 고치고 문서를 다시 생성합니다. 면접관이 두 문서를 나란히 놓는 순간, 숫자의 불일치는 "과장"으로 읽힙니다 — 실수여도요.
벽 5. 정리하고 보니 남들보다 빈약해 보여요 — 이대로 내도 되나요?
증상: 다른 사람의 화려한 포트폴리오를 보고 자괴감이 옵니다.
원인: 비교 대상의 선정 오류입니다 — 그 사람의 최종판과 여러분의 첫 통합판을 비교하고 있습니다. 그리고 화려함의 상당 부분은 여러분이 아직 안 써 본 장식입니다.
해결: 사실로 정정하세요 — 부족한 게 아니라 정리가 안 된 것이었고, 오늘 그 정리를 끝냈습니다. 기준은 하나입니다 — 모든 문장이 검증 가능한가. 검증 가능한 8편이 검증 불가능한 80편을 이깁니다. 포트폴리오는 완성품이 아니라 갱신되는 문서입니다 — 다음 대회, 다음 분석이 이 페이지의 다음 버전을 만듭니다. 오늘 필요한 것은 v1의 공개이지 완성이 아닙니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 인벤토리화 | 기억이 아니라 파일 시스템으로 세는 재고 조사 — 과소평가의 교정 |
| 대표작 선정 | 양이 아니라 깊이 + 축의 다양성(풀이/분석/제작/경쟁/전달) |
| 4줄 요약 | 문제·접근·결과·배운 것 — "배운 것"이 성장의 증거 |
| 검증 가능성 | 모든 수치는 제3자 확인 가능한 것만 — 신뢰의 본체 |
| 단일 원천 | 숫자는 records.csv 하나에서 — 문서 간 불일치 방지 |
| 이력서 문법 | "아는 것"이 아니라 "한 것" — 주장이 아니라 증거 |
오늘의 도구·명령어
| 도구·명령 | 하는 일 |
|---|---|
python step334_inventory.py <폴더> |
자산 총량·분야 분포·대회 기록 집계 + 통합 페이지 초안 생성 |
| 거울 폴더 구조 | posts/reports/repos/talks + records.csv — 증거의 집결지 |
| GitHub 프로필 README | 계정명과 같은 저장소의 특수 README — 채용 열람의 첫 화면 |
| pinned 저장소 6개 | 트로피가 아니라 증거를 고정 — README 3줄 규칙 |
| 1장 이력서 압축 | 통합 페이지의 "한 것" 문법 압축본 |
명령어보다 중요한 감각
포트폴리오는 자기 PR이 아니라 검증 가능성의 축적입니다. 오늘 만든 페이지의 가치는 디자인이 아니라, 임의의 문장 하나를 골라도 그 끝에 링크와 숫자가 붙어 있다는 구조에 있습니다. 그 구조가 만드는 것은 신뢰이고, 신뢰는 채용과 협업의 유일한 통화입니다.
그리고 이 페이지는 완성품이 아닙니다. 다음 대회의 순위, 다음 분석의 보고서, 다음 저장소가 이 페이지를 갱신합니다. 중요한 것은 오늘 갱신 가능한 형태로 만들었다는 것 — 원천이 하나이고, 구조가 잡혔고, 빈칸이 무엇으로 채워져야 하는지 안다는 것입니다. 이제 남은 것은 이 페이지를 무기로 쓸 다음 결정, 커리어 방향의 확정입니다.
전부 체크되면 Step 334 완료입니다.