Step 323. 오픈소스 도구 공개와 커뮤니티 기여 — 코드를 작품으로
Level 4 — 제보·CVE 분석·오픈소스 기여 | 난이도 ★★★☆☆ | 예상 소요 시간 3시간
전제: Step 86~87의 Git/GitHub 기초(init/commit/push/clone/bare 저장소), 이 책에서 직접 만든 도구가 하나 이상 있다.
- 준비물: Git Bash, 공개할 나만의 도구(없으면 오늘 함께 만드는 포트 스캐너
portpeek). GitHub 계정이 있으면 실제 공개, 없으면 로컬 bare 저장소로 전 과정을 동일하게 연습합니다 — 이 챕터의 실측은 로컬 시뮬레이션 방식입니다. - 주의: ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다. 공개하는 도구에는 "내 시스템·허가된 대상 전용"이라는 용도 명시가 반드시 들어갑니다.
이 책을 여기까지 오며 여러분은 스캐너, 자동화 스크립트, 분석 도구를 여럿 만들었습니다. 그것들을 정리해 공개 저장소로 올리면 세 가지를 얻습니다 — 코드 품질을 높이는 압력, 커뮤니티의 피드백, 그리고 채용 담당자가 확인할 수 있는 실력의 증거. 오늘은 도구 하나를 골라 README와 라이선스를 갖춰 공개하고, 다른 사람의 기여(수정 브랜치)를 받아 병합하는 커뮤니티 사이클 전체를 실습합니다. 좋은 오픈소스의 핵심은 코드보다 문서라는 것을 몸으로 확인하게 될 것입니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 공개할 도구를 고르고 코드를 "남이 쓸 수 있는 상태"로 정리한다
- README 여섯 칸(소개/용도 명시/설치/사용법/옵션/라이선스)을 작성한다
- MIT 라이선스를 골라 적용하고, 라이선스 선택의 기준을 설명한다
- 공개 저장소를 만들고 push한다 (GitHub 또는 로컬 bare 저장소)
- 기여자의 수정 브랜치를 받아 검토·병합하는 이슈 대응 사이클을 수행한다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | Git Bash + 파이썬 3.10+ (공개할 도구는 표준 라이브러리만 사용) |
| 오늘의 명령 | git init --bare, git remote add, git push -u, git checkout -b, git merge --no-ff |
| 필요한 개념 | README, 라이선스(MIT), 이슈와 PR(풀 리퀘스트), Step 87의 원격 저장소 |
| 오늘의 산출물 | 공개 저장소 1개(README+라이선스+용도 명시) + 기여 병합 1건 |
2-1. 왜 공개하는가 — 세 가지 수익
내 컴퓨터에만 있는 스크립트는 "돌아가면 그만"입니다. 공개하는 순간 기준이 바뀝니다 — 남이 읽고, 남이 실행하고, 남이 질문합니다. 이 압력이 코드를 좋게 만듭니다.
커리어 면에서는 이력서의 "파이썬 가능"이라는 문장보다, README와 커밋 기록이 갖춰진 저장소 주소 하나가 백 배의 증거입니다. 그리고 들어오는 이슈와 수정 제안은 무료 코드 리뷰입니다.
2-2. README — 코드보다 먼저 읽히는 것
방문자는 코드부터 읽지 않습니다. README를 5초 훑고 "쓸 만한가"를 결정합니다. README에 "무엇을, 왜, 어떻게 쓰는가"가 없으면 아무도 쓰지 않습니다.
보안 도구의 README에는 여섯 칸이 필요합니다.
| 칸 | 내용 |
|---|---|
| 한 줄 소개 | 무엇인가 |
| ⚠️ 사용 범위 | 내 시스템·허가된 대상 전용임을 명시 |
| 설치 | 필요한 것과 받는 법 |
| 사용법 | 명령 예시와 출력 예시 |
| 옵션 | 인자 표 |
| 라이선스 | 어떤 조건으로 쓸 수 있는가 |
둘째 칸이 보안 도구 특유의 칸입니다. 스캐너·테스트 도구는 용도 명시가 없으면 배포하는 순간 오용의 빌미가 됩니다.
2-3. 라이선스 — "가져가도 되는가"에 대한 답
라이선스 파일이 없는 공개 코드는 법적으로 "보기만 가능"입니다. 라이선스가 있어야 남이 쓰고 고칠 수 있습니다.
입문 단계에서의 선택은 단순합니다. MIT — "마음껏 쓰되, 저작권 표시는 남기고, 문제 생겨도 내 책임 아님" — 이 가장 널리 쓰이고, 이 책의 도구들에는 이것으로 충분합니다. GPL 계열은 "고친 코드도 공개해야 함" 조건이 붙는 복제left(copyleft) 라이선스로, 목적이 분명해지면 그때 다시 고민하면 됩니다.
2-4. 이슈와 PR — 커뮤니티의 문법
이슈(issue)는 "이거 고쳐 주세요 / 이 기능 어때요"라는 게시판 글이고, PR(pull request, 풀 리퀘스트)은 "이렇게 고쳤으니 받아 주세요"라는 수정 제안입니다. 오늘은 로컬 저장소로 이 흐름을 재현합니다 — 기여자 역할의 두 번째 클론이 버그를 고쳐 브랜치로 push하고, 원래 저장소(관리자 역할)가 검토 후 병합합니다. GitHub에서의 이슈·PR 화면은 이 흐름을 웹에서 버튼으로 하는 것일 뿐입니다.
3. 따라 하기
3-1. 공개할 도구 선정과 정리 — portpeek
공개 대상은 "가장 자주 쓰는 나만의 스크립트"가 좋습니다. 오늘은 이 책의 포트 스캐너 실습을 정리한 portpeek을 만듭니다. 공개용으로 정리한 코드입니다 — 하드코딩된 경로 없이, 인자 처리(argparse)와 에러 처리를 갖췄습니다. portpeek.py로 저장하세요.
#!/usr/bin/env python3
"""portpeek — 단일 호스트 포트 스캐너 (내 랩 전용 교육 도구)"""
import argparse
import socket
def scan(host, ports, timeout=0.5):
open_ports = []
for port in ports:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
try:
if sock.connect_ex((host, port)) == 0:
open_ports.append(port)
except socket.gaierror:
print(f"[오류] 호스트를 찾을 수 없습니다: {host}")
return []
finally:
sock.close()
return open_ports
def parse_ports(text):
if "-" in text:
a, b = text.split("-", 1)
return range(int(a), int(b) + 1)
return [int(x) for x in text.split(",")]
def main():
p = argparse.ArgumentParser(description="내 랩 전용 포트 스캐너")
p.add_argument("host", help="대상 호스트 (예: 127.0.0.1)")
p.add_argument("--ports", default="1-1024", help="예: 22,80,443 또는 1-1024")
p.add_argument("--timeout", type=float, default=0.5, help="초 (기본 0.5)")
args = p.parse_args()
print(f"대상: {args.host} / 포트: {args.ports}")
found = scan(args.host, parse_ports(args.ports), args.timeout)
if found:
for port in found:
print(f" [OPEN] {port}")
else:
print(" 열린 포트 없음")
if __name__ == "__main__":
main()
읽는 법: 공개 전 정리의 기준은 세 가지입니다 — ① 실행에 내 컴퓨터 경로가 필요 없는가, ② --help만으로 사용법이 보이는가, ③ 잘못된 입력에 파이썬 트레이스백이 아니라 사람의 말로 답하는가.
3-2. 저장소 만들고 첫 공개 커밋
입력:
mkdir portpeek && cd portpeek
git init
# portpeek.py 저장 후
printf "__pycache__/\n*.log\n.env\n" > .gitignore
git add portpeek.py .gitignore
git commit -m "portpeek 0.1.0 — 첫 공개"
git log --oneline
출력 (2026-09-09 실측):
cf71c45 portpeek 0.1.0 — 첫 공개
읽는 법: .gitignore를 첫 커밋부터 넣는 습관을 보세요 (Step 87). 버전 번호 0.1.0은 "아직 초기지만 공개했다"는 뜻의 관용적 표기입니다.
3-3. README와 라이선스 — 대문 갖추기
README.md를 이렇게 씁니다 (실측에 사용한 파일 — 각 [칸]은 실제 파일에서 ## 사용 범위 같은 마크다운 소제목이 됩니다).
# portpeek ← 파일의 제목 (마크다운 # 제목)
내 랩 전용 단일 호스트 포트 스캐너. 외부 의존성 없는 순수 파이썬(표준 라이브러리만 사용).
[⚠️ 사용 범위]
이 도구는 "자신이 소유한 시스템 또는 명시적 허가를 받은 대상"에만 사용하세요.
허가 없는 스캔은 관련 법령(정보통신망법 등)에 저촉될 수 있습니다.
[설치]
파이썬 3.10+ 만 있으면 됩니다. 별도 설치 없이 파일 하나를 받아 실행합니다.
git clone https://github.com/여러분아이디/portpeek.git
cd portpeek
[사용법]
python portpeek.py 127.0.0.1 # 1-1024 스캔 (기본값)
python portpeek.py 127.0.0.1 --ports 22,80 # 지정 포트만
python portpeek.py 127.0.0.1 --ports 1-100 # 범위 지정
python portpeek.py 127.0.0.1 --timeout 1.0 # 타임아웃 조정
[옵션] ← 표로 정리
--ports 기본값 1-1024 22,80,443 또는 1-100 형식
--timeout 기본값 0.5 포트당 대기 시간(초)
[라이선스]
MIT — LICENSE 파일 참조.
LICENSE 파일은 MIT 전문을 넣습니다 — "MIT License" 전문은 검색하면 바로 나오고, GitHub는 저장소 생성 시 버튼 하나로 넣어 줍니다 (화면 예시 — "Choose a license"에서 MIT 선택). 파일에 연도와 이름만 바꿔 쓰면 됩니다.
git add README.md LICENSE
git commit -m "docs: README와 MIT 라이선스 추가"
git log --oneline
출력 (2026-09-09 실측):
46852fe docs: README와 MIT 라이선스 추가
cf71c45 portpeek 0.1.0 — 첫 공개
3-4. 원격에 공개 — push
GitHub라면 저장소를 만들고 주소를 연결합니다 (Step 87과 동일, 화면 예시). 오늘의 실측은 로컬 bare 저장소입니다.
입력 (로컬 시뮬레이션):
cd ..
git init --bare portpeek.git
cd portpeek
git remote add origin ../portpeek.git
git push -u origin main
출력 (2026-09-09 실측):
branch 'main' set up to track 'origin/main'.
To ../portpeek.git
* [new branch] main -> main
읽는 법: 이제 이 저장소는 "공개된" 것입니다 (GitHub라면 전 세계에, bare 시뮬레이션에서는 원격으로). 공개 직후에 할 일 하나 — 다른 기기나 시크릿 창에서 저장소 주소를 열어 "남에게 보이는 모습"을 확인하세요. README가 첫 화면에 뜨는지, .env 같은 파일이 실수로 올라가지 않았는지.
3-5. 첫 이슈 도착 — 혼합 입력 버그 (실제 발견!)
공개하면 이런 일이 생깁니다. 이 챕터의 실측 준비 중 실제로 발견된 버그를 그대로 이슈로 씁니다. 이슈 본문 예시:
이슈 #1: --ports에 범위와 목록을 섞으면 죽습니다
python portpeek.py 127.0.0.1 --ports 20-25,80,443
을 실행하면 아래 오류가 납니다.
ValueError: invalid literal for int() with base 10: '25,80,443'
README의 예시에는 둘 다 있다고 나오는데, 섞어 쓰는 게 안 되는 것 같습니다.
직접 확인 (2026-09-09 실측 — 실제로 재현됩니다):
$ python portpeek.py 127.0.0.1 --ports 20-25,80,443
대상: 127.0.0.1 / 포트: 20-25,80,443
Traceback (most recent call last):
...
ValueError: invalid literal for int() with base 10: '25,80,443'
읽는 법: parse_ports()가 - 여부만 보고 처리해서, 20-25,80,443 같은 혼합 입력을 못 읽는 것입니다. 이슈가 왔을 때 관리자의 첫 행동은 "재현 확인"입니다. 좋은 이슈(재현 명령과 에러 전문 포함)는 이것만으로 절반의 수정입니다.
3-6. 기여자의 수정 — 브랜치로 받기
기여자(두 번째 클론 역할)가 이 이슈를 고쳐 보냅니다. 기여자 쪽 절차입니다.
입력:
cd ..
git clone portpeek.git contributor
cd contributor
git checkout -b fix-port-range
parse_ports()를 혼합 입력을 읽도록 고칩니다.
def parse_ports(text):
ports = []
for part in text.split(","):
if "-" in part:
a, b = part.split("-", 1)
ports.extend(range(int(a), int(b) + 1))
else:
ports.append(int(part))
return ports
수정이 동작하는지 확인하고 push합니다.
python portpeek.py 127.0.0.1 --ports 20-22,80
git add portpeek.py
git commit -m "fix: --ports에 범위와 목록 혼합 입력 지원 (#1)"
git push -u origin fix-port-range
출력 (2026-09-09 실측 — 수정 후 정상 동작):
대상: 127.0.0.1 / 포트: 20-22,80
[OPEN] 22
To .../portpeek.git
* [new branch] fix-port-range -> fix-port-range
읽는 법: 커밋 메시지 끝의 (#1)은 이슈 번호를 가리키는 관용 표기입니다. GitHub에서는 이 번호가 자동으로 이슈와 연결됩니다. 기여자는 main을 직접 건드리지 않고 브랜치로 제안합니다 — 이것이 PR의 본질입니다.
3-7. 관리자의 검토와 병합 — 이슈 대응의 완성
다시 원래 저장소(관리자 역할)로 돌아와, 들어온 브랜치를 검토하고 병합합니다.
입력:
cd ../portpeek
git fetch origin
git merge --no-ff origin/fix-port-range -m "merge: #1 포트 범위+목록 혼합 입력 지원"
git push origin main
git log --oneline
출력 (2026-09-09 실측):
45b2056 merge: #1 포트 범위+목록 혼합 입력 지원
538558c fix: --ports에 범위와 목록 혼합 입력 지원 (#1)
46852fe docs: README와 MIT 라이선스 추가
cf71c45 portpeek 0.1.0 — 첫 공개
읽는 법: --no-ff는 "기여였다"는 사실을 merge 커밋으로 역사에 남기는 옵션입니다. log에 fix(기여자)와 merge(관리자)가 나란히 쌓인 것이 보이죠 — 이 두 줄이 커뮤니티 협업의 화석입니다. 마지막으로 main에서도 다시 실행해 확인하고, 이슈에 "main에 병합했습니다. 감사합니다!"라고 닫는 댓글을 다는 것으로 사이클이 끝납니다 (GitHub 화면 예시 — Merge pull request 버튼이 이 역할입니다).
왜 하는가: 여러분은 지금 "도구를 만든 사람"에서 "기여를 받아 관리한 사람"으로 한 단계 올랐습니다. 이 경험 — 재현 확인, 수정 검토, 병합, 감사 표시 — 이 오픈소스 유지보수(maintaining)의 전부이고, 이력서의 "오픈소스 기여"가 아니라 "오픈소스 운영"이라고 쓸 수 있는 근거입니다.
4. 미션과 연습문제
미션 — 내 도구의 공개와 기여 사이클
- 이 책에서 만든 도구 중 하나를 골라 공개용으로 정리합니다 — 하드코딩 경로 제거, argparse 인자 처리, 에러 메시지 정비 (도구가 없다면 오늘의 portpeek으로 진행)
- 여섯 칸(소개/사용 범위/설치/사용법/옵션/라이선스)의 README와 LICENSE 파일을 작성합니다
- GitHub(또는 로컬 bare) 저장소에 공개하고, 다른 세션에서 "남에게 보이는 모습"을 확인합니다
- contributor 클론을 만들어 개선 하나(버그 수정, 옵션 추가, 오탈자 수정 무엇이든)를 브랜치로 push합니다
- 원래 저장소에서 검토·병합하고
git log --oneline으로 fix+merge 두 줄을 확인합니다
연습문제
문제 1. README 여섯 칸 중 보안 도구 특유의 칸은 무엇이며, 그것이 없을 때 어떤 문제가 생기나요?
문제 2. 라이선스 파일이 없는 공개 코드의 법적 상태를 설명하고, MIT 라이선스의 세 가지 조건(허용/의무/면책)을 말해 보세요.
문제 3. 기여자가 main에 바로 push하지 않고 브랜치로 보내는 이유를, 관리자의 검토 절차와 함께 설명해 보세요.
문제 4. "이 정도 코드를 공개해도 되나"라는 부끄러움에 대한 이 챕터의 답은 무엇인가요? 그리고 첫 공개가 다음 코드에 미치는 영향을 말해 보세요.
5. 모범 답안과 완료 기준
미션 모범 답안
흐름은 3번 섹션의 실측 순서 그대로입니다. 검증 명령 모음:
git log --oneline # 첫 공개 → docs → fix → merge 순으로 쌓였는가
git remote -v # origin이 등록됐는가
python portpeek.py --help # 사용법이 도구 스스로 설명하는가
README는 3-3의 여섯 칸 구조면 합격이고, 특히 ⚠️ 사용 범위 칸이 있어야 합니다. 개선 기여는 작아도 됩니다 — 3-6의 혼합 입력 수정이 오늘의 모범 사례이며, 이 버그는 이 챕터 실측 준비 중 실제로 발견된 것입니다.
검증하는 법: ① git log --oneline에 fix와 merge 커밋이 나란히 있는가. ② README에 사용법과 출력 예시가 있는가. ③ LICENSE 파일이 있고 이름·연도가 채워졌는가. ④ 공개 저장소에 비밀 파일(.env 등)이 없는가.
연습문제 해답
문제 1 해답. ⚠️ 사용 범위(합법 용도 명시) 칸입니다. 스캐너·테스트 도구는 이 명시가 없으면 악용 도구로 오인되거나 실제로 오용될 수 있고, 배포자에게 책임 논란이 올 수 있습니다. "자신의 시스템/허가된 대상 전용"이라는 한 줄이 도구의 성격을 가르는 선입니다.
문제 2 해답. 라이선스가 없는 공개 코드는 저작권법상 "열람만 가능"하고 사용·수정·재배포가 허용되지 않습니다. MIT는 ① 사용·복제·수정·배포를 무료로 허용하고, ② 저작권 표시와 라이선스 사본을 유지할 의무를 두며, ③ "있는 그대로(AS IS)" 제공되어 손해에 대한 책임을 지지 않는다는 면책 조항을 둡니다.
문제 3 해답. 브랜치는 "제안"이고 main은 "확정"이기 때문입니다. 관리자는 들어온 브랜치를 받아(fetch) 수정 내용을 검토하고 테스트한 뒤에만 병합(merge)합니다. 기여자가 main을 직접 바꿀 수 있다면 검토라는 관문이 사라집니다 — 이 구조가 낯선 사람의 코드를 안전하게 받아들이는 오픈소스의 기본 장치입니다.
문제 4 해답. 세계적 도구들도 첫 커밋은 조악했습니다. 공개는 완성 후가 아니라 성장 과정에서 하는 것입니다. 공개하면 "남이 읽는다"는 압력이 코드와 문서를 좋게 만들고, 들어오는 이슈가 다음에 고칠 것을 알려 줍니다. 부끄러움을 이긴 첫 공개가 다음 코드를 바꿉니다.
완료 기준 체크리스트
- [ ] 공개할 도구를 고르는 기준(자주 쓰는 것)을 설명할 수 있다
- [ ] 코드 정리 3조건(경로 제거/인자 처리/에러 메시지)을 적용할 수 있다
- [ ] README 여섯 칸을 채운 저장소를 만들었다
- [ ] MIT 라이선스를 적용했다
- [ ] 원격 저장소에 push하고 "남에게 보이는 모습"을 확인했다
- [ ] 기여 브랜치를 받아 검토·병합하는 사이클을 완주했다
- [ ] 미션: 내 도구 공개 + 기여 병합 1건을 완료했다
6. 흔한 실수와 해결
벽 1. 공개하고 보니 내 컴퓨터 경로가 박혀 있다
증상: 다른 사람이 실행하면 FileNotFoundError: C:\Users\내이름\...이 납니다.
원인: 개발 중 하드코딩한 절대 경로가 남은 것입니다.
해결: 3-1의 정리 기준으로 돌아가세요 — 경로는 인자나 설정 파일로 받습니다. 발견 즉시 고쳐서 커밋하면 됩니다. 이런 수정이 쌓이는 것이 버전 0.1.0 → 0.2.0의 역사입니다.
벽 2. 이슈에 에러 메시지가 없다
증상 (화면 예시): "안 돼요"라는 이슈만 달랑 왔습니다.
원인: 초보 사용자는 재현 정보를 모릅니다.
해결: 관리자의 표준 답변을 정해 두세요 — "감사합니다! 실행하신 명령 전체와, 에러 메시지 전문, 파이썬 버전을 알려 주시겠어요?" 오늘 3-5의 이슈(명령+에러 전문 포함)가 좋은 이슈의 기준이고, 그런 이슈를 유도하는 것도 관리자의 일입니다. 저장소에 이슈 템플릿을 두는 방법도 있습니다.
벽 3. 기여 브랜치를 merge했는데 충돌이 난다
증상 (출력 예시): CONFLICT (content): Merge conflict in portpeek.py
원인: 기여자가 수정하는 동안 main의 같은 부분이 바뀐 것입니다.
해결: 충돌 파일을 열어 <<<<<<<, =======, >>>>>>> 표시 사이에서 남길 쪽을 골라 정리한 뒤 add·commit합니다. Step 87~88에서 다룬 절차와 같습니다. 작은 도구에서는 기여자에게 "최신 main에서 다시 브랜치를 만들어 주세요"라고 부탁하는 것이 더 깨끗한 경우도 많습니다.
벽 4. LICENSE의 이름과 연도를 안 바꿨다
증상: Copyright (c) 2026 여러분이름이 그대로 공개됐습니다.
원인: 템플릿을 그대로 올린 흔한 실수입니다.
해결: 고쳐서 커밋하면 끝입니다. 다만 이번 기회에 확인하세요 — 공개 저장소의 모든 템플릿 파일(README의 여러분아이디 포함)은 올리기 전 "남에게 보이는 모습" 점검(3-4)에서 잡아야 할 항목입니다.
벽 5. 아무도 내 저장소를 안 본다
증상: 공개한 지 일주일, 방문자도 이슈도 없습니다.
원인: 정상입니다. 오픈소스는 공개한다고 읽히는 것이 아닙니다.
해결: 저장소의 첫 독자는 검색 엔진과 채용 담당자입니다. README 첫 줄이 "이 도구가 무엇인지"를 정확히 말하는지 다시 보세요. 그리고 반대 방향도 있습니다 — 기존 보안 오픈소스의 오탈자 수정, 문서 보완 같은 작은 PR이 여러분의 이름을 커뮤니티에 올리는 가장 빠른 길입니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| README | 저장소의 대문 — 5초 안에 읽혀야 한다 |
| 사용 범위 명시 | 보안 도구 README 특유의 칸 — 합법 용도의 선 |
| MIT 라이선스 | 자유로운 사용 + 저작권 표시 + 면책 |
| 이슈(issue) | 버그 제보·기능 제안 게시판 글 — 재현 정보가 생명 |
| PR(pull request) | 브랜치로 보내는 수정 제안 — 제안과 확정의 분리 |
| 유지보수(maintaining) | 기여를 받아 검토·병합하는 관리자의 일 |
오늘의 명령어
| 명령 | 하는 일 |
|---|---|
git init --bare 이름.git |
연습용 원격 저장소 (GitHub 대용) |
git push -u origin main |
첫 공개 push |
git clone 주소 폴더 |
기여자 역할의 두 번째 클론 |
git checkout -b 브랜치명 |
기여용 브랜치 만들기 |
git fetch origin |
들어온 기여 받아 보기 |
git merge --no-ff 브랜치 |
기여를 역사에 남기며 병합 |
명령어보다 중요한 감각
오늘의 중심 문장은 이것입니다 — 좋은 오픈소스의 핵심은 코드보다 문서다. 코드가 실력이라면 README는 그 실력의 번역이고, 라이선스는 초대장이며, 이슈 대응은 손님 접대입니다.
그리고 "이 정도를 공개해도 되나"라는 부끄러움은 모든 사람의 첫 공개 앞에 서 있는 같은 문입니다. 세계적 도구들도 첫 커밋은 조악했습니다. 공개는 완성 후가 아니라 성장 과정에서 하는 것이고, 오늘 여러분은 그 문을 — 직접 만든 저장소와, 실제로 발견해 고친 버그 하나와 함께 — 열었습니다.
전부 체크되면 Step 323 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.