코드를 가끔씩이라도 기억에 의존해 한 글자씩 직접 타이핑하는 습관이 왜 프로그래밍 실력을 더 폭넓게 끌어올리는지에 대한 글.
예전에 나는 Zed Shaw의 "Learn X the hard way" 강의들 중 하나를 읽은 적이 있다(아마 그의 C++ 강의였던 것 같다). 그는 강의를 시작하면서 대략 "예제를 복사해서 붙여넣지 말고 직접 타이핑하라"는 취지의 면책 문구를 넣었는데, 그렇게 하는 것이 자료에 대한 이해를 높여 준다고 강하게 믿었기 때문이다.
요즘 그는 그보다 더 강한 방법을 권한다:
가장 좋은 방법은 연습 문제를 하나 끝냈을 때, 방금 한 것을 지우고 다시 해보되 기억을 사용하려고 노력하는 것이다. 막히면 연습 문제를 보고 힌트를 얻되, 방금 한 일을 기억만으로 재현하려고 최선을 다해 보라. 처음에는 계속 연습 문제와 코드를 다시 보게 되겠지만, 결국에는 혼자서도 할 수 있는 지점에 도달하게 된다. 이것은 더 많은 코드를 기억하는 능력, 어떤 것이 어떻게 동작하는지 요약하는 능력, 그리고 전반적으로 한 번 하고 넘어가는 것보다 더 빠르게 실력을 향상시키는 데에도 도움이 된다.
인지심리학에서는 이것을 생성 효과라고 부른다. 같은 자료를 수동적으로 소비하는 것보다 새로운 내용을 능동적으로 생성하는 편이 이해를 더 높여 준다는 뜻이다. 혹은 Richard Feynman의 말대로다: "내가 만들어낼 수 없는 것은, 내가 이해하지 못한 것이다."
나는 연구자는 아니지만, 일반 프로그래머의 입장에서 왜 이 현상이 프로그래밍 능력 향상에 중요하다고 믿는지 나름의 관점을 덧붙이고 싶다. 심지어 agentic coding의 시대인 지금은 이것이 오히려 그 어느 때보다 더 중요하다고 생각한다. 그렇지 않으면 결국 코딩 에이전트의 도움 없이 사람이 대체 어떻게 프로그래밍했는지 의아해하는 프로그래머 세대 전체가 생겨날 것이기 때문이다.
이 글에서는 왜 여러분이 가끔씩이라도 가능한 한 기억에 의존해 코드를 한 글자씩 직접 타이핑해야 하는지에 대한 근거를 설명하려 한다. 그렇다고 해서 코딩 과정을 효율화해 주는 각종 개발자 도구를 전혀 쓰지 말아야 한다는 뜻은 아니다. 다만 그런 보조 수단 없이 코딩하는 능력도 가끔은 의도적으로 늘려야 한다는 뜻이다.
나는 프로그래밍할 때 세부 사항을 가끔씩이라도 직접 풀어 써 보는 것이 중요하다고 믿는다. 그리고 여기서 말하는 세부 사항은 성능 특성이나 UI 설계 결정 같은 것만이 아니다. 다음과 같은 아주 기본적인 것들을 뜻한다:
… 즉 키워드, 구두점, 언어 구성 요소를 유창하게 다루는 능력
… 타입 시스템과 데이터 모델에 대한 친숙함과 편안함을 포함해서
… 함수, 메서드, 클래스, import, 파일 이름을 정확히 떠올리는 능력 같은 것들
이런 것들이 여러분의 머릿속에 들어 있어야 기억만으로 "프리코딩"할 수 있다. 내 경험상 이렇게 하는 프로그래머는 프리코딩 숙련도와 겉보기에는 무관해 보이는 작업에서도 다른 프로그래머보다 훨씬 뛰어난 성과를 낸다. 그리고 나는 이것이 단순한 상관관계라고도 생각하지 않는다. 오히려 인과관계라고 본다: 프리코딩은 더 넓은 프로그래밍 탁월함을 길러 준다.
왜 그럴까? 여러분이 머릿속에 떠올린 것을 어느 정도의 정밀도로1 타이핑해 낼 수 없다면, 그것은 애초에 정말로 머릿속에 있었던 것이 아니기 때문이다. LLM과 똑같이 이해하고 있다고 착각한 것이다.
"하지만 Gabby,"라고 여러분은 반박할지도 모른다. "나는 문법이나 이름 같은 세부 사항에 발목 잡히지 않고도 고수준에서 코드를 능숙하게 이해하고 다룰 수 있다고 확신하는데."
아니, 나는 그것이 어떤 사람들이 생각하는 만큼 잘 작동한다고 확신하지 않는다. 왜 그런지 이제 설명해 보겠다.
먼저 문법부터 시작해 보자. 아마 내가 반복 연습하라고 주장하는 것들 가운데 가장 논쟁적인 주제일 것이다. 결국 지금은 2026년이고, IDE나 코딩 에이전트가 문법은 알아서 맞춰 줄 수 있는데 왜 우리가 굳이 문법을 신경 써야 하느냐는 것이다.
우선, 문법에 어려움을 겪는다면 나는 여러분이 다른 많은 것들(심지어 프로그래밍이 아닌 것들까지)도 어려워할 것이라고 추측하겠다. 예를 들어 괄호를 맞춰 쓰는 데 자주 애를 먹는다면, 나는 여러분이 다른 사람의 논리적 전제를 결론과 얼마나 유창하게 연결할 수 있는지 의문을 갖게 될 것이다.
나는 더 나아가 세부 사항을 무시하는 태도를 "기능적으로는 표현 불능"인 사람들의 유행병과도 연결하고 싶다. 즉, 문장을 논리적으로 따라가거나 이어 붙이지 못하는 사람들 말이다. 단어의 의미를 맑은 정신으로 따져 가며 추론하기보다, 분위기만으로 소통하는 부류의 사람들을 말한다.
내가 무슨 뜻인지 보여 주기 위해, 최근에 나는 어떤 LLM 프롬프트를 검토하다가 이런 문구를 발견했다:
Never suggest external tools or alternatives that aren't part of the skills listed above. If a task requires capabilities beyond the available skills, say so.
나는 이런 종류의 프롬프트를 기능적으로는 표현 불능이라고 읽는다. 자세히 읽지 않으면 그럴듯하게 들리지만, 자세히 보면 실제로는 모델에게 정반대로 충돌하는 두 가지 지시를 내리고 있기 때문이다.
내가 보기에는 기능적으로는 표현 불능인 사람들은 거의 예외 없이 스스로 철자를 맞추거나 문법적으로 올바른 문장을 만드는 데에도 어려움을 겪는다. 왜냐하면 작은 것을 큰 것과 분리할 수 없기 때문이다. 세부 사항을 정확히 맞추는 일을 대충 넘기기 시작하면, 그 태도는 여러분이 하는 모든 일로 번져 나가고 결국 큰 그림까지도 틀리게 보게 된다.
마찬가지로, 문법에 약한 사람들은 추상적 사고에도 약한 경우가 많다. 그들은 아마 문법을 큰 그림 사고를 방해하는 사소한 디테일로 여길 것이다. 하지만 사실은 정반대다! 문법은 고차원적 사고를 압축하고 강화하는 정신적 도구이며, 추론을 위한 정밀하고 명료하며 압축적인 내부 언어를 우리에게 제공한다.
다르게 말하자면, 문법은 이것과
xis a an array of objects, each of which has a requireddomainproperty storing a string and an optionalportproperty storing a number
… 이것의 차이다:
x : { domain: string, port?: number }[]
… 그리고 이것이 내가 여러분이 다음으로 반복 연습해야 한다고 생각하는 주제로 이어진다:
자기 손등을 아는 것만큼이나2 타입 시스템과 데이터 모델을 잘 알아야 한다는 점은 아무리 강조해도 지나치지 않다. Fred Brooks가 The Mythical Man Month에서 말했듯이:
Show me your flowchart and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won't usually need your flowchart; it'll be obvious.
예를 들어 데이터베이스를 사용한다면, 프로젝트의 테이블과 컬럼 이름 그리고 그 관계를 완전히 꿰고 있어야 한다. 이런 정보를 "필요할 때마다" 게으르게 조회하지 말라. 효과적인 시스템 수준 설계를 하려면 이런 정보를 미리 적극적으로 수집해 항상 머릿속 최상단에 올려 두어야 한다. 이런 수고를 들이지 않는 사람은 자기 생각만큼이나 혼란스러운 데이터베이스 스키마를 만든다. 정규화가 안 되어 있고, 중복되거나 비슷한 데이터로 가득하며, 단일 진실 공급원도 없다.
이것은 타입과 타입 시스템에도 그대로 적용된다! 아주 강한 정적 타입 언어(예: Rust나 Haskell)를 사용해 본 사람이라면 강한 타입 정신 모델을 유지하는 이점을 증언할 수 있다. 게다가 정확한 "머릿속 타입 검사기"나 "머릿속 borrow checker"를 기르는 훈련은 약한 타입 언어로 프로그래밍할 때조차도 도움이 된다3. 그런 정신적 근육을 훈련하면 프로젝트를 추상적으로 추론하는 능력이 향상되고, 그렇지 않으면 잘못된 상태를 표현 불가능하게 만들기 같은 데이터 모델링의 기본에서조차 자꾸 걸려 넘어진다.
모든 타입이 어떻게 맞물리는지 이해하지 못하면 무슨 일이 일어나는지 아는가. TypeScript 코드 곳곳에 as any 타입 단언을 뿌리기 시작한다. 여러분(또는 여러분의 코딩 에이전트)이 마주친 오류를 어떻게 고쳐야 할지 모르기 때문이다. 결국, 순탄한 경로(코드 작성)에서 타입을 끝까지 생각해 보는 불편함조차 견디지 못한다면, 순탄하지 않은 경로(타입 오류 디버깅)는 절대 버티지 못한다.
나는 또한 여러분이 프로젝트나 의존성 안에서 자주 쓰이는 함수/메서드/클래스/import/패키지/파일 이름을 쉽게 떠올릴 수 있어야 한다고 믿는다. 그리고 프로젝트가 발전함에 따라 그 지식도 최신 상태로 유지해야 한다.
이것은 더 일반적인 규칙의 특수한 사례다. 여러분은 선행 사례에 익숙해져야 한다. 오픈 소스 세계에 이미 있는 것이든, 여러분 조직 내부에 이미 있는 것이든, 누군가가 이미 만들어 놓은 것들 말이다. 사람들이 코딩 에이전트에 기대어 일을 시키는 이유 중 상당수는 자신이 원하는 것을 해 주는 재사용 가능한 선행 사례를 모르기 때문이다. 그래서 결국 에이전트에게 바퀴를 다시 발명하라고 시키게 되고, 에이전트는 기꺼이 그렇게 한다.
예를 들어 SaaS boilerplate projects가 존재한다는 사실을 모른다면, 여러분은 그 모든 것을 코딩 에이전트가 대신 비계 작업해 줘야 한다고 생각할 것이다. 바로 그런 목적을 위해 만들어지고 실전 검증까지 거친 오픈 소스 프로젝트를 clone하는 편이, 에이전트에게 같은 일을 시키는 것보다 더 빠르고, 더 저렴하며, 더 신뢰할 만하다4.
같은 프로젝트나 회사 내부에서 코드를 재사용할 때도 마찬가지다. 이름을 기억하는 데 약하다면, 동료의 코드를 재사용하는 데에도 약할 것이다. 무엇을 찾아야 하는지조차 모른다면 다른 사람의 작업을 토대로 무언가를 쌓아 올릴 수 없다.
물론 에이전트가 그 부분을 도와줄 수는 있다. 하지만 이제 새로운 문제가 생긴다. 여러분은 에이전트의 출력을 정말로 의미 있게 검토할 수 있는가? 예를 들어, 다른 기능이 뭐가 있는지도 모른다면 코딩 에이전트가 기능을 중복 구현하고 있는지 어떻게 알아차릴 수 있는가? 에이전트보다 코드를 더 못 이해한다면, 그 출력물의 품질을 어떻게 검토할 수 있는가5?
에이전트에게 테스트를 생성하라고 시킬 수도 있다. 하지만 내 경험상 많은 agentic 코더들은 그 테스트조차 꼼꼼히 검토할 생각이 없다. 그래서 결국 다음과 같은 테스트 코드가 나오게 된다:
// This is a real example I've seen in the wild
describe("abort detection logic", () => {
it("detects aborted stopReason in messages", () => {
const messages = [
{ role: "assistant", stopReason: "aborted", content: [] },
];
const isAborted = messages.some((m: any) => m.stopReason === "aborted");
expect(isAborted).toBe(true);
});
it("detects abort in error string", () => {
const error = "The operation was aborted";
const isAborted = error.includes("abort");
expect(isAborted).toBe(true);
});
it("does not false-positive on normal errors", () => {
const error = "Network timeout";
const isAborted = error.includes("abort");
expect(isAborted).toBe(false);
});
it("does not false-positive on normal stop reasons", () => {
const messages = [
{ role: "assistant", stopReason: "stop", content: [] },
];
const isAborted = messages.some((m: any) => m.stopReason === "aborted");
expect(isAborted).toBe(false);
});
});
소프트웨어 개발은 일상적인 마찰로 가득 차 있고, 우리가 작은 마찰에 어떻게 반응하느냐가 더 큰 마찰에 어떻게 반응하느냐에 영향을 준다. 작은 마찰(예: 이름 하나 기억하기)은 넘어설 가치가 없다고 결정하면, 결국 더 큰 마찰(예: 테스트를 면밀히 검토하기) 역시 넘어설 가치가 없다고 결정하게 된다.
여기까지의 예시들에는 공통된 줄기가 있다는 점을 눈치챘을지도 모른다:
Eustress는 좋다. 새로운 것을 하거나 배우도록 스스로에게 도전하면 더 작은 불편함을 견디는 내성이 쌓이고, 더 큰 불편함을 극복할 길도 닦인다. 늘 불편함을 피하기만 하면, 점점 더 큰 무기력과 좌절의 악순환으로 빠져든다.
한 가지를 아주 잘하게 되면 그것이 다른 것들을 더 잘하게 되는 데에도 번져 나간다. 모든 것이 연결되어 있기 때문이다. LLM이 학습 데이터로부터 일반화하듯이, 인간도 그렇다! 한 영역에서 정밀함, 기억력, 구조화된 사고를 기르면 다른 영역에서도 같은 종류의 향상이 일어난다.
이 글이 마음에 들었다면, 내가 쓴 다른 글인 Software engineers are not (and should not be) technicians도 흥미로울 수 있다. 그 글에서는 소프트웨어 개발이라는 일이 본질적으로 자신의 안전지대를 반복해서 벗어나는 일을 포함한다고 설명한다.
중요한 점은, 영어는 정밀한 언어가 아니라는 것이다. unless you contort it into something as detailed as code. ↩
반론: 사실 나는 내 손등도 그렇게 잘 알지는 못한다 ↩
그렇다고 해서 내가 Haskeller나 Rustacean이 다른 사람들보다 꼭 더 좋은 코드를 쓴다고 믿는다는 뜻은 아니다(가끔 정말 형편없는 코드를 쓰는 것도 본 적이 있다, 솔직히). 하지만 다른 조건이 같다면 타입에 능숙한 편이 확실히 도움이 된다 ↩
예전에 나는 누군가가 몇 년 안에 LLM이 브라우저 전체를 만들 수 있게 될 것이라고 예측하는 것을 본 적이 있다. 내가 든 생각은 오직 이것뿐이었다: "나는 이미 오늘 당장 Chromium을 fork해서 그걸 할 수 있는데" 그리고 그 편이 수정하고 유지보수하기도 훨씬 쉬울 것이다. ↩
또 하나, 코딩 에이전트는 자신을 다루는 사람들의 정신적 습관을 그대로 흡수한다. 에이전트에게 내리는 지시는 에이전트 컨텍스트의 큰 부분을 차지하며, 여러분이 일관되게 지적으로 게으르다면 에이전트를 똑같이 지적으로 게으른 골짜기로 곧장 몰아넣게 된다. 에이전트가 아닌 비유를 들자면, 지적으로 게으른 관리자는 지적으로 게으른 보고 라인을 만들어 낸다. ↩