Skip to content
Published on

남이 쓴 코드 읽는 법

Authors

들어가며 — 우리는 쓰기보다 읽는다

새 팀에 합류한 첫 주를 떠올려 봅시다. 50만 줄짜리 코드베이스, 처음 보는 프레임워크, 위키는 2년 전에 멈춰 있고, 그 코드를 짠 사람은 이미 퇴사했습니다. 당신이 할 일은 이 안에서 버그 하나를 고치는 것입니다. 어디서부터 시작해야 할까요.

흔히 프로그래밍을 "코드를 쓰는 일"이라고 생각하지만, 실제로 개발자가 시간을 쓰는 비율은 정반대입니다. 널리 인용되는 어림값으로 읽기와 쓰기의 비율이 대략 10 대 1이라고들 합니다. 기능 하나를 추가하려면 그 주변의 기존 코드를 읽어 이해해야 하고, 버그를 고치려면 원인 코드를 읽어야 하고, 리뷰를 하려면 남의 코드를 읽어야 합니다. 우리는 쓰는 사람이라기보다 읽는 사람입니다.

그런데 이상하게도 코드 읽기는 아무도 체계적으로 가르쳐 주지 않습니다. 쓰기는 강의도 책도 넘치는데, 읽기는 "그냥 하다 보면 는다"고 방치됩니다. 이 글은 낯선 코드베이스를 마주했을 때 쓸 수 있는 구체적인 전략들을 정리합니다. 핵심 한 줄로 요약하면 이렇습니다. 모든 줄을 읽으려 하지 말고, 데이터가 흐르는 길을 따라가라.

진입점을 찾아라

코드를 읽을 때 가장 먼저 할 일은 **진입점(entry point)**을 찾는 것입니다. 프로그램이 실제로 실행을 시작하는 지점 말입니다. 아무 파일이나 열어 위에서부터 읽는 건 지도 없이 숲 한가운데 떨어지는 것과 같습니다.

진입점은 프로그램 종류에 따라 다릅니다.

  • CLI 도구: main() 함수, 또는 package.jsonbin, pyproject.toml의 스크립트 정의.
  • 웹 서버: 라우팅 테이블. "이 URL로 요청이 오면 이 핸들러가 처리한다"는 매핑이 지도 역할을 한다. 특정 기능을 이해하고 싶으면, 그 기능의 API 경로부터 찾아 해당 핸들러로 들어간다.
  • 프런트엔드 앱: 루트 컴포넌트(예: App)와 라우터 설정. 특정 화면이 궁금하면 그 라우트에 연결된 컴포넌트부터.
  • 라이브러리: 공개 API. index 파일이나 패키지 매니페스트의 exports가 "바깥에서 쓰는 문"이다. 여기서부터 안쪽으로 파고든다.

진입점을 찾는 실용적인 요령: 에러 메시지, 로그 문구, UI에 보이는 텍스트를 grep하라. 화면에 "결제 실패"라는 문구가 뜬다면, 그 문자열을 코드에서 검색해 보세요. 대개 원하는 기능의 바로 그 지점으로 순간이동합니다. 이건 정말 강력한데도 자주 잊히는 기법입니다.

모든 줄이 아니라 데이터를 따라가라

낯선 코드를 처음부터 끝까지 한 줄도 빠짐없이 읽으려는 시도는 거의 항상 실패합니다. 며칠을 쏟아도 머릿속에 그림이 안 그려집니다. 이유는 단순합니다. 코드의 대부분은 지금 당신이 풀려는 질문과 무관하기 때문입니다.

훨씬 나은 전략은 **데이터를 따라가는 것(follow the data)**입니다. 특정 값 하나를 정하고, 그것이 어디서 태어나(입력·생성), 어떤 변환을 거쳐(가공), 어디로 가는지(저장·출력) 그 흐름만 쫓습니다. 나머지 코드는 일단 무시합니다.

예를 들어 "업로드한 이미지가 왜 회전돼서 저장되지?"를 조사한다면:

  1. 이미지가 들어오는 지점을 찾는다(업로드 핸들러).
  2. 그 이미지 데이터가 어떤 변수에 담기는지 보고, 그 변수를 따라간다. 어떤 함수로 넘겨지는가?
  3. 넘겨진 함수 안에서 이미지가 어떻게 변형되는가(리사이즈, 포맷 변환, EXIF 처리...).
  4. 최종적으로 어디에 저장되는가.

이 사슬만 따라가면, 50만 줄 중 실제로 읽는 건 수십 줄입니다. 회전 문제라면 3번 단계의 EXIF 처리 근처에서 범인이 나올 겁니다. 관계없는 인증 로직, 결제 로직, 관리자 페이지는 단 한 줄도 안 읽어도 됩니다.

데이터를 따라갈 때 유용한 사고 도구가 **자료의 형태(shape)**입니다. 각 단계에서 "지금 이 값은 무슨 타입이고, 어떤 구조지?"를 계속 물으세요. 여기선 파일 경로 문자열, 여기선 바이트 버퍼, 여기선 디코드된 픽셀 배열... 형태의 변화를 추적하면 흐름이 선명해집니다.

테스트와 타입을 문서로 삼아라

공식 문서는 대개 낡았거나 없습니다. 하지만 거짓말을 하지 않는 두 종류의 문서가 코드 안에 이미 있습니다. 테스트와 타입입니다.

테스트 = 실행 가능한 명세

어떤 함수가 뭘 하는지 알고 싶으면, 그 함수의 테스트를 먼저 읽으세요. 테스트는 "이런 입력을 주면 이런 출력이 나온다"를 구체적인 예시로 보여 주는, 실행 가능한 사용 설명서입니다. 게다가 CI가 통과하고 있다면 그 설명은 지금 이 순간 입니다. 낡은 위키와 달리, 테스트는 코드와 함께 검증됩니다.

테스트에서 특히 값진 것:

  • 엣지 케이스: describe/it 블록의 이름들이 그 함수가 신경 쓰는 경계들을 나열해 줍니다. "빈 배열일 때", "음수일 때", "동시 요청일 때" — 개발자가 무엇을 걱정했는지 드러납니다.
  • 사용 예시: 테스트의 준비(arrange) 부분은 그 함수를 실제로 어떻게 호출하는지 보여 줍니다. API 사용법을 문서 대신 여기서 배웁니다.

기능을 이해하고 싶은데 테스트가 있다면, 그 테스트를 하나 골라 디버거로 스텝을 밟아 보세요. 실행되는 코드를 따라가며 읽는 것만큼 빠른 이해는 없습니다.

타입 = 계약

정적 타입이 있는 코드(TypeScript, Rust, Go, 타입 힌트가 붙은 Python 등)라면, 타입 시그니처가 곧 계약서입니다. 함수의 몸통을 읽기 전에 시그니처만 봐도 절반은 이해됩니다. 무엇을 받고(입력 타입), 무엇을 돌려주는지(반환 타입)가 명시돼 있으니까요.

타입은 데이터의 형태를 코드로 못 박아 둔 것이기도 합니다. 앞서 말한 "데이터를 따라가기"를 할 때, 각 단계의 타입을 IDE로 확인하면 형태 변화가 그대로 보입니다. 타입이 없는 언어라도, 함수 초입의 검증 로직이나 문서 주석이 비슷한 역할을 합니다.

콜 그래프를 그려라 — grep과 IDE 점프

코드를 읽다 보면 끊임없이 마주치는 질문이 둘 있습니다. "이 함수는 어디서 정의됐지?"와 "이 함수는 누가 부르지?" 이 두 질문에 빠르게 답하는 능력이 코드 읽기 속도를 좌우합니다.

  • 정의로 점프(go to definition): 어떤 심볼이 어디서 정의됐는지로 순간이동. IDE의 이 기능은 코드 읽기의 기본기입니다. 위에서 아래로(호출하는 쪽 → 호출되는 쪽) 파고들 때 씁니다.
  • 참조 찾기 / 호출자 찾기(find references / find callers): 반대로, 어떤 함수가 어디어디서 쓰이는지 전부 찾기. 아래에서 위로(정의 → 사용처) 거슬러 올라갈 때 씁니다. "이 함수를 고치면 뭐가 영향받지?"를 물을 때 필수.

IDE가 없거나, 여러 언어가 섞여 IDE가 못 따라가거나, 서버에서 코드만 열려 있을 때는 **grep(또는 ripgrep, rg)**이 만능 열쇠입니다.

# 이 함수가 어디서 호출되는지 전부 찾기
rg "processPayment\("

# 이 문자열(에러 메시지, 라벨)이 코드 어디에 있는지
rg "결제 실패"

# 이 환경 변수를 누가 읽는지
rg "STRIPE_SECRET_KEY"

# 특정 확장자만, 줄 번호와 함께
rg -t ts "useAuth" -n

머릿속으로 이 점프들을 반복하다 보면 콜 그래프가 그려집니다. "A가 B를 부르고, B가 C와 D를 부르고, C는 여기저기서 불린다." 복잡한 흐름은 종이나 화이트보드에 이 그래프를 실제로 그려 보세요. 노드 대여섯 개만 그려도 머리가 훨씬 맑아집니다.

이런 grep·점프 워크플로를 git 히스토리 탐색과 함께 손에 익히고 싶다면 Git 실습장에서 브랜치를 오가며 코드가 어떻게 변해 왔는지 추적하는 연습을 해 볼 수 있습니다. git log -S"함수이름"으로 "이 코드가 언제 추가됐는지"를 찾는 것도 코드 읽기의 강력한 무기입니다.

실제로 돌려 보고 print를 찍어라

정적으로 읽기만 해서는 안 풀리는 지점이 반드시 옵니다. 코드가 "무엇을 할 수 있는가"는 읽어서 알지만, "실제로 이번엔 무엇을 했는가"는 돌려 봐야 압니다. 조건 분기가 복잡하거나, 값이 런타임에 결정되거나, 콜백·이벤트로 흐름이 튀면 특히 그렇습니다.

그래서 일단 돌리세요. 그리고 궁금한 지점에 print(또는 로그)를 찍어 실제 값을 눈으로 확인하세요.

  • "여기 정말 도달하나?": 도달 여부가 의심스러운 코드에 print("HERE reached")를 찍는다. 안 찍히면 그 분기는 안 타는 것이고, 당신의 흐름 이해가 틀린 것이다.
  • "이 값이 여기서 뭐지?": 변수 값을 라벨과 함께 찍어 형태와 내용을 확인한다. 머릿속 추측과 실제가 다른 경우가 놀랄 만큼 많다.
  • 호출 순서: 여러 함수에 진입 로그를 찍으면 실제 실행 순서가 드러난다. 비동기 코드에서 특히 유용하다.

이건 앞서 다룬 "데이터를 따라가기"의 동적 버전입니다. 정적으로 흐름을 추적하다 막히면, 그 지점에 print를 박아 실제 데이터를 잡아냅니다. 읽기와 실행을 번갈아 하는 게 낯선 코드를 이해하는 가장 빠른 길입니다.

뭔가를 바꾸고 무엇이 깨지는지 보라

코드를 이해하는 놀랍도록 효과적인, 그러나 과소평가된 방법이 있습니다. 일부러 뭔가를 바꿔 보고, 무엇이 깨지는지 관찰하는 것입니다.

어떤 함수, 어떤 설정, 어떤 상수가 무슨 역할을 하는지 확신이 안 서면, 그걸 바꿔 보세요. 값을 뒤집고, 줄을 주석 처리하고, 상수를 이상한 값으로 바꾼 뒤 돌려 봅니다. 그러면 시스템이 반응으로 답을 줍니다.

  • 어떤 줄을 지웠는데 아무것도 안 변한다 → 그 줄은 (적어도 이 경로에선) 중요하지 않다. 죽은 코드일 수도 있다.
  • 상수를 바꿨더니 특정 화면이 깨진다 → 그 상수는 그 화면과 연결돼 있다. 연결 관계를 하나 알아냈다.
  • 함수 반환값을 하드코딩했더니 테스트 세 개가 실패한다 → 그 세 테스트가 이 함수에 의존한다. 영향 범위가 드러난다.

이건 능동적 실험입니다. 코드를 수동적으로 바라보는 대신, 시스템에 질문을 던지고 답을 받는 겁니다. 물론 안전하게 하세요. 프로덕션이 아니라 로컬에서, git으로 언제든 되돌릴 수 있는 상태에서요. git stash나 브랜치를 활용하면 실컷 부순 뒤 원상복구하는 게 공짜입니다. "이해가 안 되면 부숴 보고 되돌린다"는 코드 읽기의 강력한 루프입니다.

위에서 아래로 읽고, 그다음 파고들어라

낯선 코드베이스를 이해하는 순서는 **하향식(top-down)**이 대체로 옳습니다. 세부부터 파고들면 나무만 보다 숲을 놓칩니다. 큰 그림을 먼저 잡고, 필요한 부분만 골라 깊이 들어가세요.

권장하는 순서는 이렇습니다.

  1. 디렉터리 구조를 훑는다. 폴더 이름만 봐도 관심사의 분리가 보인다. api/, db/, components/, services/... 어디에 무엇이 사는지 지도를 만든다.
  2. README, 설정 파일, 매니페스트를 읽는다. package.json/pyproject.toml의 의존성 목록은 "이 프로젝트가 무슨 도구로 뭘 하는지"를 압축해 보여 준다. 스크립트 정의는 "이걸 어떻게 빌드·실행·테스트하는지"를 알려 준다.
  3. 아키텍처의 층을 파악한다. 요청이 들어와서 나갈 때까지 어떤 층을 지나는가(라우터 → 컨트롤러 → 서비스 → 저장소 같은). 이 뼈대를 잡으면 새 코드를 만나도 "아, 이건 서비스 층이구나" 하고 자리를 잡을 수 있다.
  4. 이제 특정 기능 하나를 골라 데이터를 따라 깊이 판다. 여기서 앞서 말한 진입점 찾기, 데이터 추적, print, grep이 총동원된다.

핵심은 넓게 얕게 → 좁게 깊게의 순서입니다. 처음부터 한 함수에 매몰되면, 그게 전체에서 어떤 위치인지 몰라 방향을 잃습니다. 지도를 먼저 그리고, 그 위에서 한 길을 골라 걸어 내려가세요.

종합 — 낯선 버그를 30분 만에

조각을 하나의 흐름으로 엮어 봅시다. 첫 주, "결제 후 확인 이메일이 가끔 안 온다"는 버그를 받았다고 합시다.

  1. 큰 그림(하향식): 디렉터리를 훑어 services/email/, services/payment/이 있음을 확인. 이메일과 결제가 분리돼 있구나.
  2. 진입점 + grep: 확인 이메일에 들어갈 법한 문구("주문이 완료되었습니다")를 grep. 이메일 템플릿과 그걸 보내는 함수 sendOrderConfirmation을 찾는다.
  3. 호출자 찾기: sendOrderConfirmation이 어디서 불리는지 참조 찾기. 결제 성공 핸들러에서 불린다.
  4. 데이터 따라가기: 결제 성공 → 이메일 발송으로 이어지는 사슬을 따라간다. 중간에 "이메일 발송을 큐에 넣는" 단계가 보인다.
  5. 테스트 읽기: 이 발송 로직의 테스트를 읽으니 "큐가 가득 차면 조용히 버린다"는 케이스가 있다. 냄새가 난다.
  6. 바꿔 보기 + print: 큐 적재 지점에 로그를 찍고 재현을 시도. 트래픽이 몰릴 때 큐가 넘쳐 일부 이메일이 버려지는 걸 눈으로 확인.

30분 전만 해도 50만 줄의 미지였던 코드에서, 관계있는 수십 줄만 읽고 원인을 짚었습니다. 한 줄도 빠짐없이 읽으려 했다면 첫날이 다 갔을 겁니다.

마치며

코드 읽기는 재능이 아니라 기술이고, 기술이니 방법이 있습니다. 핵심 원칙을 다시 모으면 이렇습니다.

  • 아무 데나 열지 말고 진입점부터 찾는다.
  • 모든 줄이 아니라 데이터가 흐르는 길만 따라간다.
  • 테스트와 타입을 거짓말 안 하는 문서로 쓴다.
  • grep과 IDE 점프로 콜 그래프를 그린다.
  • 정적으로 막히면 돌려 보고 print를 찍는다.
  • 확신이 안 서면 바꿔 보고 무엇이 깨지는지 본다.
  • 위에서 아래로 큰 그림을 먼저, 그다음 깊이.

다음에 낯선 코드베이스 앞에서 막막할 때, 첫 파일을 열어 위에서부터 읽으려는 충동을 참으세요. 대신 물으세요. "진입점이 어디지? 내가 따라갈 데이터가 뭐지?" 이 두 질문이 미지의 숲에 낸 첫 번째 길입니다.

참고 자료