Step 323. 오픈소스 도구 공개와 커뮤니티 기여 — 코드를 작품으로

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*.logn.envn" > .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. 미션과 연습문제

미션 — 내 도구의 공개와 기여 사이클

  1. 이 책에서 만든 도구 중 하나를 골라 공개용으로 정리합니다 — 하드코딩 경로 제거, argparse 인자 처리, 에러 메시지 정비 (도구가 없다면 오늘의 portpeek으로 진행)
  2. 여섯 칸(소개/사용 범위/설치/사용법/옵션/라이선스)의 README와 LICENSE 파일을 작성합니다
  3. GitHub(또는 로컬 bare) 저장소에 공개하고, 다른 세션에서 "남에게 보이는 모습"을 확인합니다
  4. contributor 클론을 만들어 개선 하나(버그 수정, 옵션 추가, 오탈자 수정 무엇이든)를 브랜치로 push합니다
  5. 원래 저장소에서 검토·병합하고 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 완료입니다.