logo

[번역] 독학 프런트엔드 개발자들을 위한 프로그래밍 원칙

더 나은 코드를 작성하는 데 도움이 되는 실행 가능한 경험 법칙들

January 13, 2026

원문: https://piccalil.li/blog/programming-principles-for-self-taught-front-end-developers/#from-pithy-statements-to-actual-useful-habits

독학 프런트엔드 개발자들을 위한 프로그래밍 원칙

많은 프런트엔드 개발자들과 마찬가지로, 저는 정규 컴퓨터 공학 배경이 없습니다. 최종 결과물에 대한 더 많은 통제권을 원했던 디자이너로서 이 분야에 뛰어들었고, ICT 학사 학위를 받긴 했지만 실제 수업 내용은, 음, "컴퓨터 공학 기초" 라는 관점에서 보면 상당히 가벼웠습니다. 이는 제가 소프트웨어 개발에 대해 아는 모든 것을 다양한 출처로부터 일하면서 배웠다는 것을 의미합니다. 만약 당신도 그렇다면, 이 글이 몇 년의 시간을 아껴줄 수 있기를 바랍니다.

20년이 넘는 시간 동안, 제 일상 업무에 가장 큰 영향을 미친 것은 UML을 사용해 OOP 시스템을 모델링하는 방법을 배웠다거나 모나드가 무엇인지 아는 것이 아니었습니다. 대신, "프로그래밍 원칙"이라는 범주에 속하는 (때로는 간결한) 여러 문구들이었습니다.

세상에는 정말 많은 프로그래밍 원칙들이 있습니다. 일부는 시스템과 사람들이 어떻게 행동하는지를 설명하는 "법칙"에 가깝습니다(호프스태터의 법칙을 고려하더라도, 항상 예상보다 더 오래 걸린다 는 호프스태터의 법칙처럼). 이런 법칙들은 더 넓은 맥락에서 유용하지만, "좋은 코드"를 작성하고자 할 때 실행 가능한 지침으로는 그다지 도움이 되지 않습니다.

이 글에서는 코드를 작성하는 동안 더 나은 코드를 작성하는 데 도움이 되는 경험 법칙들을 다룹니다. 이 원칙들은 유용해지기 위해 전체 시스템을 미리 작성할 필요가 없으며, 그저 진행하면서 더 나은 결정을 내리는 데 도움을 줍니다.

간결한 문구에서 실제로 유용한 습관으로

개발을 막 시작했을 때, 어느 순간 누군가가 당신의 코드를 가리키며 "성급한 최적화는 모든 악의 근원이다"라고 말할 것입니다. 이는 매우 심각하게 들리지만, 동시에 다소 이상하게 느껴지기도 합니다.

최적화는 좋은 것이지만 "성급함"은 보통 그렇지 않습니다. 여기서 큰 문제는 당신이 하고 있는 것이 악이라는 말을 들었지만, 실제로 다음에 무엇을 해야 할지 모른다는 것입니다. 성급한 최적화를 피하는 것은 좋은 프로그래밍 원칙이지만, 실제로 더 나은 코드를 작성하는 데 도움이 되지는 않습니다.

경험 많은 시니어 개발자가 나서서 YAGNI(You Aren’t Gonna Need It, 필요 없을 것이다) 원칙을 설명해줄 수도 있습니다. 이는 미래에 필요할 것으로 예상되지만 지금은 필요하지 않은 코드를 작성하는 것에 대해 경고하는 또 다른 좋은 프로그래밍 원칙입니다. 왜냐하면 지금은 필요하지 않은 상태와, 훗날 필요할 것이라 계획한 시점 사이에서 계획이 바뀔 가능성이 매우 높고, 그 결과 결국에는 실제로 필요하지 않게 되는 경우가 많기 때문입니다. 따라서 실제로 필요할 때 "적시에" 작성하는 것이 더 낫습니다.

하지만 동시에 DRY(Don't Repeat Yourself, 반복하지 마라) 원칙을 따르라는 요구도 받게 됩니다. 같은 일을 하는 코드를 여러 곳에 작성하지 말라는 것입니다. 유지보수가 어렵기 때문이죠. 이 말을 따른다면, 당신이 할 일은 같은 일을 하는 코드를 단일 함수나 모듈로 통합하고 여러 곳에서 그것을 호출하는 것뿐입니다.

이 모든 약어들은 일반적으로 좋은 조언입니다. 어떤 기능을 구축하고 있다면, 미래를 어느 정도 예상하는 것이 합리적이지 않을까요? 여러 (미래의) 상황에서 그 로직이 필요하다면, 이미 그것을 최적화하거나 리팩토링하는 것이 합리적입니다. 그렇게 하면 현재 코드가 그것을 고려하도록 할 수 있고, 두 번 작성할 필요가 없습니다.

YAGNI와 성급한 최적화가 언급되는 이유는 보통 지금 필요한 기능을 위한 코드를 작성하는 대신, 지금 필요한 기능뿐만 아니라 미래에 필요할 수도 있는 기능까지 제공하는 일반적인 시스템을 위한 코드를 작성하기 시작하기 때문입니다. 이는 훨씬 더 많은 코드를 작성하고, 훨씬 더 많은 복잡성을 추가하며, 시니어 동료들보다 훨씬 더 오래 걸리게 된다는 것을 의미합니다.

이런 원칙들은 실제로 코드를 작성할 때 도움이 되지 않습니다. 나중에 필요할 것 같은 임의의 부분들을 빼놓을 수는 있지만, 무엇을 빼놓아야 할지 알려주는 원칙이 있다면 훨씬 더 좋을 것입니다.

여기서 "3의 법칙"이 등장합니다 — YAGNI, DRY, 성급한 최적화에 대해 어떻게 생각해야 하는지를 우아하게 결합한 실제로 실행 가능하고 실용적인 원칙입니다.

3의 법칙은 같은 코드를 세 번 작성한 후에만 리팩토링(또는 최적화)해야 한다고 말합니다. 처음 코드를 작성할 때는 그냥 작성합니다. 그것이 하는 일만 하도록 말이죠. 두 번째로 같은 코드가 다시 필요하다는 것을 알게 되면, 말 그대로 복사-붙여넣기를 하고 필요한 몇 가지 변경사항을 적용합니다. 그리고 세 번째로 그것을 해야 할 때, 그제야 이제 세 가지 구현을 살펴보고 세 가지 케이스를 모두 처리할 수 있는 단일 구현으로 일반화합니다.

여기서의 개념은 코드를 세 번 작성하고 나면, 실제로 필요한 일반적인 기능이 무엇인지, 어떤 부분을 단순화하고 최적화할 수 있는지 이해하게 된다는 것입니다. 세 가지 구현을 거친 후에는 필요한 "추상화 수준"이 무엇인지 알게 되고, 일반화된 구현이 실제로 세 가지 케이스 모두에 대해 작동하는지 확인할 수 있습니다. 저는 이 원칙을 항상 "적용"합니다. 너무 쉽기 때문이죠(셋까지 셀 수 있으니까요!). 그리고 이것은 제가 막 과도한 엔지니어링을 하려고 할 때 그것을 피하도록 도와줍니다.

프로그래밍 원칙에 대해 읽다 보면, 매우 심각하게 들리지만 실제로 적용하려면 많은 맥락이 필요한 법칙과 규칙들을 쉽게 찾을 수 있습니다. 위의 예시에서 일반적인 "성급한 최적화는 모든 악의 근원이다"에서 구체적인 "3의 법칙"으로 넘어가는 것이 일상적인 코딩과 작성하는 코드의 품질에 훨씬 더 큰 영향을 미친다는 것을 알 수 있기를 바랍니다.

올바른 것을 작성하기

이제 첫 번째 구현에 좀 더 집중해 보겠습니다. 처음부터 코드를 최적화하여 각 줄이 빠르고 효율적인지 확인하고 싶은 유혹이 매우 클 수 있습니다. 하지만 대부분의 경우, 빠르고 효율적인 코드가 가장 읽기 쉬운 코드는 아닙니다.

코드를 작성할 때, 실제로 작성하는 데 그렇게 많은 시간을 쓰지 않습니다. 대신, 방금 작성한 코드나 이미 존재하는 코드를 읽고 그것에 대해 추론하여 다음에 무엇을 작성할지 결정하는 데 시간을 보냅니다. 따라서 코드를 읽고 추론하기가 쉬울수록 올바른 코드를 더 빠르게 작성할 수 있습니다. 빠르고 최적화된 코드는 훌륭하지만, 읽고 추론하기 어렵다면 장기적으로는 우리를 느리게 만들 것입니다.

하지만 우리는 모든 것을 원합니다. 올바른 코드를 빠르게 작성하고 싶고, 빠른 코드를 작성하고 싶습니다. 그렇다면 어떻게 해야 할까요? 어떻게 선택해야 할까요?

여기서 저는 또 다른 원칙을 적용하는 것을 좋아합니다. "작동하게 만들고, 올바르게 만들고, 빠르게 만들어라"라는 원칙입니다. 이는 익스트림 프로그래밍 의 창시자인 켄트 벡(Kent Beck)의 말로 알려져 있습니다.

코딩하는 동안 언제든지 코드를 보고 스스로에게 물어볼 수 있습니다. 작동하는가? 이 질문에 대한 답이 아니오라면, 코드가 작동하도록 만드는 데 집중합니다. 다른 것은 신경 쓰지 않습니다. 그냥 작동하게 만드세요. 작동한다면, 좋습니다!

하지만 작동하는 것이 항상 올바른 일을 하는 것은 아니므로, 다음으로는 올바른가? 를 물어봅니다. 코드가 올바르지 않은 한(원하는 대로 동작하지 않거나, 테스트를 실패하게 만들거나, 입력을 받아들이지 않는 한), 올바르게 작동하도록 만드는 데 집중합니다. 그런 다음 코드가 작동하고 코드가 올바를 때만, 빠른가? 를 물어봅니다. 충분히 빠르지 않다면, 이제 빠르게 만드는 데 집중할 수 있습니다.

이것은 노력의 우선순위를 정하는 데 도움이 됩니다. 아직 작동하지 않거나 잘못된 일을 하는 코드를 최적화하는 것은 시간 낭비입니다. 어쨌든 다시 작성해야 하고 그러면 최적화도 사라집니다. 코드가 아직 작동하지도 않는다면, 망가진 코드가 올바른지 아닌지 걱정할 필요가 없습니다. 망가져 있으므로 유용한 것을 하기 전에 먼저 작동하게 만들어야 합니다.

이 원칙이 매우 유용한 이유는 코드(의 동작)를 보는 것만으로도 언제든지 적용할 수 있다는 것입니다. 아무것도 미리 계획할 필요가 없습니다. 그저 이 세 가지 질문을 순서대로 스스로에게 하고, 지금 중요한 것에 집중하면 됩니다. 관련이 있을 때까지 다른 모든 것은 잊어버릴 수 있습니다.

자세히 보면 이것이 실제로는 3의 법칙과 같은 원칙이며, 단지 프로그래밍의 다른 측면에 적용된 것임을 알 수 있습니다. 둘 다 지금 가지고 있는 작업에 집중하고, 아직 관련이 없는 다른 우려사항들로 인해 주의가 산만해지는 것을 피하도록 도와줍니다.

형편없는 첫 번째 버전

첫 번째 구현을 작성하는 사고방식을 갖도록 도와주는 여러 원칙들이 있습니다. 이러한 맥락의 원칙들은 첫 번째 구현을 버리라고 말하거나, 지금은 최선의 간단한 시스템을 구축하라고 말합니다. 실제로 3의 법칙에서 "작동하게 만들어라"는 것이 바로 이와 같은 아이디어입니다.

이것의 더 고상한 버전도 있습니다. 갈의 법칙(Gall's Law) 이라고 불리는데, "작동하는 복잡한 시스템은 예외 없이 작동했던 단순한 시스템에서 진화한 것으로 밝혀진다"고 말합니다.

여기서의 아이디어는 처음부터 모든 것을 고려하려고 하면, 결국 만들어진 것이 실제로는 작동하지 않는다는 것입니다. 이것의 예는 지금까지의 모든 "전체 재작성"이 원본보다 더 나쁘게 끝나고 계획보다 훨씬 더 오래 걸린 경우입니다. (물론 이것에도 두 번째 시스템 효과라는 이름이 있습니다)

이 모든 것을 읽으면 KISS(Keep It Simple, Stupid, 단순하게 유지하라, 이 바보야) 원칙이 떠오를 수도 있습니다. 하지만 KISS의 문제는 사람들이 어떤 작업을 수행하도록 돕는 무언가를 만들고 있다면 일정 수준의 복잡성은 피할 수 없다는 점입니다. 그리고 그런 상황이 되면, KISS는 더 이상 답을 주지 못한다는 것입니다. 어떤 것들은 단순하지 않습니다. 반면에 갈의 법칙(Galls' Law) 은 적어도 단순하게 시작하고 목표를 향해 반복하는 것에 대해 솔직하다면 복잡성이 존재할 수 있다고 말해줍니다.

그럼에도 불구하고, 이것은 코드를 작성할 때 실행하기 쉽진 않으므로, 실제로 코드를 작성할 때 더 나은 코드를 작성하는 데 도움이 되는 몇 가지 원칙을 찾을 수 있는지 봅시다.

멱등성

가능한 한, 제가 작성하는 함수들은 멱등성 을 갖습니다. 큰 단어죠, 그렇지 않나요? 실제로 이것이 의미하는 것은 함수가 같은 인자를 받았을 때 항상 같은 일을 한다는 것뿐입니다. 획기적으로 들리지 않을 수 있지만, 실제로는 코드에 대해 추론하기가 얼마나 쉬운지에 대해 상당히 큰 영향을 미칩니다.

멱등성을 가진 함수가 있다면, 같은 인자로 여러 번 호출해도 항상 같은 결과를 반환합니다. 예를 들어, 문자열의 길이를 가져오는 것은 멱등적입니다. "hello".length를 몇 번을 호출하든 항상 5를 반환합니다. 함수가 멱등하다는 것을 알면, 그것이 몰래 새 문자열을 반환하거나 코드의 다른 곳에서 변수를 읽는 것에 대해 걱정할 필요가 없습니다. 같은 인자가 들어가면, 같은 결과가 나옵니다.

참고

멱등 함수와 순수 함수 사이에는 미묘한 차이가 있습니다. 이 부분과 특별히 관련이 있는 것은 아니지만 정말 알고 싶다면, 순수 함수는 부수 효과가 없다는 것이 보장됩니다. 즉, 전역 데이터에 접근하거나 수정하지 않으며, 인자를 제자리에서 변경하지도 않습니다.

멱등한 함수는 부수 효과가 멱등적인 한 여전히 부수 효과를 가질 수 있습니다. 함수를 여러 번 호출해도 항상 같은 값이 설정되는 한, 전역 상태나 데이터베이스 항목을 변경하는 것은 멱등적일 수 있습니다.

그렇지 않은 경우, 함수를 두 번 호출하면(사용자가 더블 클릭하거나 불안정한 연결에서 서버의 재시도 로직으로 인해) 결과 값이 변경될 수 있습니다. 이는 코드에 대한 추론을 훨씬 더 어렵게 만듭니다. 부수 효과가 필요하다면, 그것을 별도의 함수로 분리한 다음 그것을 멱등적으로 만들 수 있습니다. 하나의 큰 함수 대신, 각각 한 가지 일을 하는 두 개의 작은 함수를 갖게 됩니다.

멱등 함수를 블랙박스처럼 취급하여 더 큰 시스템에 대해 추론하는 동안 구현 세부사항을 "잊을" 수 있습니다. 이는 더 높은 수준에서 추론하기 쉽게 만드는 두뇌 지름길과 같습니다. 함수는 항상 같은 일을 하므로, 마음속에서 그것을 단일 단계로 축약할 수 있습니다.

단일 책임 원칙

멱등성과 관련된 것은 단일 책임 원칙(Single Responsibility Principle) 입니다. 이 원칙은 함수(또는 모듈, 또는 클래스)가 "변경될 단 하나의 이유"만 가져야 한다고 말합니다.

실제로 이것은 시스템의 한 측면을 담당하는 하나의 함수가 있어야 한다는 것을 의미합니다. 여기서 좋은 예는 모든 데이터베이스 접근을 처리하기 위해 ORM(Object-Relational Mapping) 을 사용하는 것입니다. ORM 모듈은 데이터베이스와 통신하는 책임을 지고 나머지 코드는 그것이 어떻게 작동하는지 전혀 알 필요가 없어야 합니다. 데이터베이스와 통신하는 방법을 변경해야 한다면, ORM만 업데이트하면 되고 코드의 다른 부분은 건드릴 필요가 없습니다.

다시 말하지만, 이것은 시스템에 대해 추론할 때 시스템의 부분들을 축약할 수 있게 해줍니다. SQL 쿼리를 어떻게 구성하는지 알 필요가 없습니다. 그것은 지금 작업하고 있는 코드의 책임이 아닙니다. ORM을 당신을 위해 데이터베이스 접근을 하는 블랙박스처럼 취급하면 됩니다.

단일 책임 원칙이 "함수/모듈/클래스는 존재할 단 하나의 이유만 가져야 한다"로 설명되는 것도 볼 수 있습니다. 그 단일 이유가 사라지면, 해당 함수/모듈/클래스를 완전히 제거할 수 있어야 합니다. 예를 들어 다른 데이터베이스로 전환하고 새 ORM이 필요하다면, 이전 ORM은 완전히 제거 가능해야 합니다.

하지만 실제로는 ORM의 기능이 점점 늘어나는 경우가 많습니다. 데이터베이스에서 데이터를 가져오기만 하는 것이 아니라, 나머지 코드가 예상하는 특정 형식으로 그것을 변환하기도 합니다. 이제 데이터베이스를 전환하려면, 그 특정 데이터 형식에 의존하는 모든 코드도 업데이트해야 합니다. ORM이 변경될 여러 이유를 갖게 되고, 이는 추론하기 더 어렵게 만듭니다.

대신, DB에서 데이터를 가져오는 것만 처리하는 하나의 모듈과 데이터 포맷팅만 처리하는 또 다른 모듈을 가져야 합니다. 이제 각 모듈은 단일 책임을 가지며, 다른 것에 영향을 주지 않고 하나를 변경(또는 삭제!)할 수 있습니다. 이것이 실행 중인 단일 책임 원칙입니다.

단일 책임을 확인하는 작은 트릭은 함수/모듈/클래스가 하는 일을 한 문장으로 설명하는 것입니다. "그리고" 라는 말을 하고 있다면, 아마도 여러 책임을 가지고 있을 것이기에 분리해야 합니다.

단일 추상화 수준

단일 책임 원칙과 관련된 것은 함수가 한 가지 일만 해야 할 뿐만 아니라, 단일 추상화 수준에서만 작동해야 한다는 개념입니다. "추상화 수준"이라는 표현 자체가 상당히 추상적인 개념입니다. 용어의 중복 사용을 양해해주세요. 이것이 의미하는 바는 함수의 코드를 읽을 때, 모든 연산이 동일한 세부 수준에 있어야 한다는 것입니다.

예를 들어, 이 함수를 보세요.

async function processUsers() {
  const users = await database.fetchAllUsers();
 
  users.forEach(user => {
    if (user.isActive) {
      sendEmail(user.email, 'Hello active user!');
    }
  });
}

이 함수에는 세 가지 추상화 수준이 있습니다.

  1. 데이터베이스에서 데이터 가져오기 (database.fetchAllUsers()).
  2. 모든 사용자를 반복하고 활성 사용자만 필터링하기 (if (user.isActive)).
  3. 해당 데이터를 기반으로 작업 수행하기 (sendEmail(...)).

이 함수를 설명하는 데 많은 "그리고"를 사용해야 할 뿐만 아니라, 데이터베이스에 연결해서 사용자를 가져오고 필터링하고 필터링된 사용자에게만 이메일을 보낸다라고 설명해야 합니다. 이 함수에 대해 이해하려면 데이터베이스, 활성 사용자, 이메일 전송이라는 세 가지 다른 세부 수준의 개념을 동시에 생각해야 합니다. 실제 코드에서는 더 많은 await, 유효성 검사, 에러 처리가 있을 수 있습니다.

함수를 읽다 보면, 집중해야 할 대상이 계속 바뀌면서 흐름을 따라가기가 점점 어려워집니다. 예를 들어, 처음에는 이 함수가 모든 사용자를 가져오는 함수처럼 보이지만, 이어서 그 사용자들을 필터링하고, 결국에는 필터링된 사용자에게만 이메일을 보내는 함수가 됩니다.

다음은 여러 추상화 수준을 나타내는 몇 가지 징후들입니다.

  • 함수가 하는 일을 설명하기 위해 여러 "그리고"가 필요함.
  • 데이터에 대한 여러 개의 루프나 반복.
  • 데이터베이스 접근과 같은 "저수준" 연산과 비즈니스 로직의 혼합.

이것을 분리하면, 각각 단일 추상화 수준에서 작동하는 세 개의 함수를 가질 수 있습니다.

async function getActiveUsers() {
  const users = await database.fetchAllUsers();
  return users.filter(user => user.isActive);
}
 
function sendEmailsToUsers(users) {
  users.forEach(user => {
    sendEmail(user.email, 'Hello active user!');
  });
}
 
function processUsers() {
  const activeUsers = getActiveUsers();
  sendEmailsToUsers(activeUsers);
}

getActiveUsers 함수는 활성 사용자를 가져오고 필터링하는 것만 다루고, sendEmailsToUsers 함수는 이메일 전송만 다루며, processUsers 함수는 모든 활성 사용자를 가져온 다음 모두에게 이메일을 보냅니다. 각 함수는 단일 추상화 수준만 다루기 때문에 이해하기 쉬우며, 각 부분에 대해 개별적으로 추론할 수 있습니다.

리팩토링된 버전에서 sendEmailsToUsers가 사용자가 어디서 왔는지 또는 그들의 상태가 무엇인지 신경 쓰지 않는다는 것을 주목하세요. 그저 주어진 사용자들에게 이메일을 보낼 뿐입니다. 각 함수는 단일 책임을 가지며 단일 추상화 수준에서 작동합니다.

모두 같은 것인가?

이 시점에서, 이러한 원칙들 중 많은 것이 서로 관련되어 있다는 것을 알아챘을 것입니다. 그리고 그 이유는 고전 명언을 좀 비틀어 표현하자면 다음과 같습니다.

모든 좋은 코드는 비슷하다. 각각의 나쁜 코드는 각자의 방식으로 나쁘다.

— 톨스토이 (만약 그가 소프트웨어 엔지니어였다면)

좋은 코드는 추론하기 쉽고, 각 부분이 무엇을 하는지 명확할 때 사물은 추론하기 쉽습니다. 부분들이 서로 어떻게 관련되는지도 명확하므로, 이 모든 원칙들은 추론하기 쉬운 코드를 작성하는 것의 서로 다른 측면을 강조합니다.

반면에, 어떤 코드가 왜 나쁜지 파악하기는 어려울 수 있습니다. 여러 책임을 갖고 있어서인가요? 최적화되어 있지만 잘못되어서인가요? 줄별로 읽을 때 여러 추상화 수준을 투어시키나요? 무엇을 위한 것인지 확실하지 않은 추가 작업을 하나요? 이 모든 것들이 나쁜 코드를 다루기 매우 어렵게 만듭니다.

나쁜 코드를 갖고 있다가 다시 좋게 만드는 것보다 위의 원칙들을 (독단적으로) 고수함으로써 좋은 코드로 끝나는 것이 훨씬 더 쉽습니다.

다음에 새 함수를 작성할 때, 이러한 원칙들에 대해 생각하고 있는 자신을 발견하기를, 아직 작동하지 않는 코드를 최적화하는 것을 멈추고, 각 함수가 한 가지 일만 하도록 확인하며, 작동하는 간단한 시스템을 갖기 전에 복잡한 시스템을 설계하는 것을 피하기를 바랍니다. 행운을 빕니다!

참고문헌 및 추가 자료

말했듯이, 저는 이 모든 것을 현장에서 배웠습니다. 다음은 제가 그 과정에서 도움을 받은 자료들입니다.