README.md에 알파벳순으로 모아 둔 거대한 큐레이티드 리스트입니다. 흥미로운 건 정작 코드는 README가 아니라 그것을 '관리'하는 src/ 폴더의 파이썬에 있다는 점 — 1,600줄짜리 목록을 사람 손으로 정렬·중복 제거하는 건 불가능하니까요. 이 글은 단순 소개를 넘어, "거대한 크라우드소싱 목록을 어떻게 자동화로 유지하는가"를 해부합니다. (저장소: emmabostian/developer-portfolios · Markdown + Python · ★약 24k · TrendShift 6위 · 명시적 LICENSE 파일 없음)이 레포가 무엇을 하는 물건인가.
개발자가 자기 이력·프로젝트·연락처를 보여주려고 만든 개인 웹사이트(포트폴리오)들을 한곳에 모아 둔 영감용 목록입니다. 새 포트폴리오를 만들 때 "다른 사람들은 어떤 구성·디자인·기술을 썼나"를 둘러보는 출발점이 됩니다.
구조 자체는 단순합니다. README.md 파일 하나에 - [이름](포트폴리오 주소) [직함 | 전문 분야] 형식의 줄이 1,800여 개 들어 있고, 알파벳 A부터 Z까지 섹션으로 나뉘어 있습니다. 누구나 자기 포트폴리오를 PR (Pull Request, 코드 병합 요청)로 추가할 수 있습니다.
하지만 이 레포가 공부 자료로 가치 있는 진짜 이유는 따로 있습니다. 1,800여 개의 줄을 사람이 알파벳순으로 유지하고 중복을 걸러내는 건 불가능합니다. 그래서 이 레포는 src/ 폴더에 그 일을 대신 해 주는 파이썬 자동화 스크립트를 두고 있습니다. 즉 "콘텐츠는 마크다운, 엔지니어링은 파이썬"인 셈이죠.
awesome-*라는 이름을 씁니다. 도서관 사서가 좋은 책만 골라 진열대를 만드는 것과 비슷합니다.트렌딩 이유 · 비슷한 목록과 다른 점.
이 레포는 2019년 개발자 Emma Bostian이 동료 Ali Spittel의 트윗("포트폴리오 좀 보여줄 사람?")에서 영감을 받아 시작했습니다. 그 뒤 전 세계 개발자들이 자기 포트폴리오를 PR로 보태면서 1,800여 개까지 불어난 거대한 크라우드소싱 자료가 되었습니다. 인기 비결은 세 가지입니다.
첫째, 구직·이직 시즌의 실용성. "나도 포트폴리오를 만들어야 하는데 뭘 넣지?"라는 막막함을 1,800여 개의 실제 사례로 단번에 풀어 줍니다. 둘째, 오픈소스 입문자의 첫 PR 연습장. 코드를 몰라도 마크다운 한 줄만 추가하면 되니, 생애 첫 기여를 여기서 해 보는 사람이 많습니다. 셋째 — 그리고 이 글의 초점 — 거대 목록을 자동화로 유지하는 모범 사례라는 점입니다.
| 비교 항목 | 보통의 awesome 목록 | 이 레포의 접근 |
|---|---|---|
| 정렬 | 기여자가 알아서 알맞은 위치에 끼워 넣기(자주 깨짐) | alphabetical.py가 전체를 다시 정렬 |
| 중복 | 리뷰어가 눈으로 확인 | URL을 정규화해 자동 탐지·제거 |
| 표기 통일 | 제각각(대소문자·이모지·공백) | 타이틀 케이스·이모지 제거 자동 적용 |
| 데이터 활용 | 마크다운 그대로 | feed.json으로 기계가 읽을 형태 제공 |
기여자마다 "Aaron"을 'B' 섹션에 넣거나, 같은 사이트를 두 번 올리거나, 이름에 이모지를 붙입니다. 리뷰어가 매번 눈으로 잡아야 하고, 목록이 커질수록 무조건 깨집니다.
PR이 들어올 때마다 스크립트가 정렬·중복 제거·표기 통일을 결정론적으로 처리합니다. 사람은 "이 사이트가 진짜 포트폴리오인가"라는 판단에만 집중하면 됩니다.
콘텐츠 · 자동화 · 데이터 · CI 한눈에.
화려한 프레임워크는 없습니다. 핵심은 파이썬 표준 라이브러리만으로 짠 텍스트 처리 파이프라인 — 외부 패키지 설치가 전혀 필요 없다는 점이 오히려 배울 만합니다.
| 레이어 | 기술 / 도구 | 역할 |
|---|---|---|
| 콘텐츠 | Markdown (README.md) | 1,800여 개 포트폴리오 항목을 담는 단일 원본. |
| 자동화 | Python 3 (re·urllib.parse·unicodedata·json·collections) | 정렬·중복 제거·표기 통일·피드 생성. |
| 데이터 | JSON (feed.json) | 마크다운에서 추출한 기계 판독용 구조화 데이터. |
| 테스트 | unittest (run_tests.py) | tests/ 폴더를 자동 탐색해 실행. |
| CI | GitHub Actions (link-checker.yml + parking-redirect-checker.yml) | 죽은 링크(404) 및 파킹 도메인 리다이렉트 자동 점검. |
목록의 포트폴리오 주소들을 직접 집계해 봤습니다(약 1,000개 표본 기준 — 전체 1,800여 개의 일부라 비율은 근사값입니다). 실제 개발자들이 개인 사이트를 어디에 올리는지 보여 주는 살아있는 데이터입니다.
| 호스팅 플랫폼 | 대략 비중 | 특징 |
|---|---|---|
Vercel (*.vercel.app) | 약 200건 (최다) | Next.js·React 배포에 강한 무료 정적/서버리스 호스팅. |
GitHub Pages (*.github.io) | 약 108건 | 깃허브 저장소를 그대로 웹사이트로. 가장 전통적인 무료 선택지. |
Netlify (*.netlify.app) | 약 68건 | 정적 사이트·JAMstack 배포의 대표 주자. |
Firebase (*.web.app) | 약 10건 | 구글의 호스팅 + 백엔드 서비스. |
| Render·Cloudflare Pages 등 | 각 수 건 | onrender.com·pages.dev 등 신흥 무료 호스팅. |
| 커스텀 도메인 | 나머지 다수 | .dev·.me·.com 직접 구매. is-a.dev 같은 무료 서브도메인도 인기. |
"포트폴리오 = 정적 사이트 한 장"이라는 공식이 데이터로 드러납니다. 무거운 서버 없이 HTML/CSS/JS를 빌드해 CDN에 올리는 Vercel·Netlify·GitHub Pages가 압도적입니다. 즉 입문자가 "포트폴리오 어디에 올리지?"를 고민한다면, 이 셋 중 하나면 충분하다는 뜻입니다.
PR 한 건이 들어오면 무엇이 어떤 순서로 도는가.
이 레포의 "아키텍처"는 서버 구조가 아니라 데이터(=README) 가공 파이프라인입니다. 먼저 전체 그림을 보고, 그다음 줄 한 개가 어떻게 처리되는지 끝까지 따라가 보겠습니다.
alphabetical.py의 9단계 (해피 패스)이 스크립트의 main()은 README를 읽어 아래 순서로 가공한 뒤 다시 저장합니다. "줄 하나"가 거치는 여정을 따라가 봅시다.
여기서 가장 영리한 부분은 ⑤ 중복 판단입니다. https://Me.com/과 http://me.com은 글자만 보면 다르지만 사실 같은 사이트죠. 그래서 비교 전에 URL 정규화를 거칩니다 — 대소문자를 낮추고, :80/:443 같은 기본 포트를 떼고, 끝의 /를 제거해 "같은 주소"로 만든 뒤 비교합니다.
중복 제거(⑤⑥)를 먼저 하고 정렬(⑧)을 한 뒤, 정렬로 나란히 붙게 된 중복을 한 번 더 잡는(⑨) 2단 구조입니다. 책장을 정리할 때 "먼저 명백한 중복을 버리고 → 가나다순으로 꽂고 → 나란히 선 같은 책을 마지막에 한 번 더 확인"하는 것과 똑같은 순서입니다.
어떤 파일이 무슨 일을 하나.
| 파일/폴더 | 역할 |
|---|---|
README.md | 단 하나의 진실 원본(single source of truth). 1,800여 개 포트폴리오 항목이 모두 여기서 나온다. |
src/alphabetical.py | 레포의 심장. 정렬·중복 제거·표기 통일·피드 생성을 한 번에 처리. |
src/run_dup_report.py | 파일을 고치지 않고 중복 현황만 출력. "수정 전 미리보기" 역할. |
src/remove_emoji_in_readme.py | 이모지 제거. dry_run(미리보기)·.bak 백업 옵션 내장. |
feed.json | {"name","url","tagline"} 배열. 다른 앱이 목록을 읽어 쓰기 좋게. |
.github/workflows/ | GitHub Actions 설정. 링크 생존 여부를 자동 검사. |
README.md는 원장(원본 데이터), src/는 그 원장을 깨끗하게 유지하는 사서 로봇들, feed.json은 외부에 내보내는 복사본입니다. 데이터는 한 곳(README)에만 있고 나머지는 거기서 파생됩니다 — 진실의 원본을 하나로 두는 깔끔한 설계입니다.
이 작은 레포에서 의외로 많이 배운다.
_normalize_url()은 텍스트 비교의 함정을 보여 주는 좋은 예제입니다. 표준 라이브러리 urllib.parse로 URL을 부품(스킴·도메인·경로)으로 쪼갠 뒤 표준형으로 다시 조립합니다. src/alphabetical.py를 읽어 보세요.
핵심 로직은 이렇게 짧습니다(읽기 쉽게 단순화):
# 겉모습이 달라도 같은 사이트로 인식되게 표준형으로 변환
def _normalize_url(url):
p = urlparse(url) # URL을 부품으로 분해
scheme = p.scheme.lower() # http/https 소문자로
netloc = p.netloc.lower() # 도메인 소문자로
path = p.path.rstrip('/') # 끝의 / 제거 → 핵심!
return urlunparse((scheme, netloc, path, '', p.query, p.fragment))
복사해서 그대로 돌려도 됩니다. https://Me.com/도, http://me.com도 모두 http(s)://me.com으로 맞춰져 "중복"으로 잡히는 원리입니다.
이름을 타이틀 케이스(aaron → Aaron)로 바꾸되 DevOps·GitHub·API처럼 대문자가 의미 있는 단어는 건드리지 않는 예외 처리가 핵심입니다. 무작정 .title()을 쓰면 Github·Api처럼 망가지죠.
# 두 번째 글자 이후에 대문자가 있거나(GitHub) 전부 대문자(API)면 그대로 둠
if len(token) > 1 and (any(c.isupper() for c in token[1:]) or token.isupper()):
result.append(token) # DevOps, GitHub, API → 보존
else:
result.append(token.title()) # aaron → Aaron
validate_section_placement()는 É로 시작하는 이름을 NFD 정규화로 분해해 기본 글자 E를 뽑아내고, 그 사람을 'E' 섹션에 배치합니다(unicodedata.normalize('NFD', ...)). 다국어 데이터를 다룰 때 반드시 만나는 문제입니다.
run_dup_report.py는 파일을 고치지 않고 중복 현황만 보여 줍니다. remove_emoji_in_readme.py에도 dry_run 옵션과 .bak 백업이 있죠. "파괴적 작업은 항상 미리보기·되돌리기를 곁들인다"는 실무 습관을 작은 코드로 배울 수 있습니다.
run_tests.py는 unittest 디스커버리로 tests/를 자동 실행하고, link-checker.yml은 GitHub Actions로 죽은 링크를 잡습니다. "사람이 매번 확인하지 않아도 규칙이 지켜지는" 구조를 직접 볼 수 있는 미니 사례입니다.
돌려 보려면 무엇이 필요한가 — 거의 아무것도 필요 없다.
| 항목 | 요구사항 |
|---|---|
| 런타임 | Python 3.6+ (f-string·표준 라이브러리만 사용) |
| 외부 패키지 | 없음 — pip install 불필요. re·json·urllib 등 내장 모듈만 사용. |
| 도구 | Git, 텍스트 에디터(또는 GitHub 웹 편집기) |
| OS | macOS · Linux · Windows (플랫폼 무관) |
| 기여 시 | GitHub 계정 (포크·PR용) |
의존성이 0개라는 점은 그 자체로 학습 포인트입니다. "표준 라이브러리만으로 충분한 일에 굳이 패키지를 늘리지 않는다"는 절제를 보여 줍니다.
읽기만 하지 말고 직접 돌려 보자.
CONTRIBUTING.md대로 포크 → add/내이름 브랜치 생성 → README의 알맞은 알파벳 위치에 - [이름](주소) [직함] 한 줄 추가 → PR. 생애 첫 오픈소스 기여 연습으로 딱입니다.
레포를 클론한 뒤 python src/generate_feed.py 실행 → feed.json이 생성되는 걸 확인. 정규식이 - [name](url) [tagline]을 어떻게 파싱하는지 코드와 결과를 대조해 보세요.
python -m src.run_dup_report로 현재 README의 중복 현황을 출력. URL 기반 중복과 인접 중복이 각각 몇 건인지 읽고, _normalize_url이 왜 필요한지 체감해 보세요.
_normalize_url에 "www. 접두사 제거" 규칙을 추가해 보고, tests/에 그 동작을 검증하는 단위 테스트를 작성한 뒤 python src/run_tests.py로 통과시켜 보세요.
이 글 3번 섹션의 "호스팅 분포"를 직접 재현해 보세요. README에서 URL을 뽑아 도메인을 집계하는 파이썬 스크립트를 짜면, 정규식·collections.Counter·데이터 집계를 한 번에 연습할 수 있습니다.
이 레포를 발판 삼아 한 주씩.
| 주차 | 주제 | 학습 자료 / 할 일 |
|---|---|---|
| 1주차 | Markdown + Git/PR 기초 | CONTRIBUTING.md 따라 실습 1 완료. 포크·브랜치·PR 흐름 체득. |
| 2주차 | 정규표현식 (re 모듈) | alphabetical.py의 패턴 분석 + 파이썬 공식 re 문서. |
| 3주차 | URL·유니코드 정규화 | urllib.parse·unicodedata 문서. 실습 4 진행. |
| 4주차 | 테스트(unittest) + CI | tests/ 읽기, GitHub Actions link-checker.yml 분석. |
| 5주차 | 내 포트폴리오 만들기 | Vercel/Netlify/GitHub Pages 중 하나로 정적 사이트 배포. |
본문에 나온 용어 빠른 참조.
| 용어 | 의미 |
|---|---|
| 큐레이티드 리스트 | 사람이 직접 골라 모은 주제별 링크 모음. GitHub의 awesome-* 레포가 대표적. |
| PR (Pull Request) | "내 변경분을 원본에 합쳐 달라"는 요청. 오픈소스 기여의 기본 단위. |
| 정규화 (normalization) | 겉모습이 달라도 본질이 같은 값을 하나의 표준형으로 맞추는 것. |
| 멱등성 (idempotent) | 같은 작업을 여러 번 해도 결과가 변하지 않는 성질. |
| 타이틀 케이스 | 각 단어 첫 글자를 대문자로(aaron → Aaron). 고유명사는 예외 처리 필요. |
| NFD 정규화 | 유니코드를 "기본 글자 + 악센트 기호"로 분해하는 방식. É → E + ´. |
| 정적 사이트 / JAMstack | 서버 없이 미리 만든 HTML/CSS/JS를 CDN에 올리는 웹사이트 방식. |
| CDN | Content Delivery Network. 콘텐츠를 전 세계 서버에 복제해 빠르게 전달. |
| CI | Continuous Integration. 변경마다 검사를 자동으로 돌려 품질을 지키는 장치. |
| dry run | 실제로 바꾸지 않고 "이렇게 바뀐다"만 미리 보여 주는 실행 모드. |