친절한 코드

우현민·2024년 2월 9일
19

dev

목록 보기
8/9
post-thumbnail
post-custom-banner

친구들과 오랜만에 여행을 갑니다. 숙소에 도착해서 불을 켜기 위해 전등 스위치를 찾아봅니다. 현관 바로 옆 벽에 있는 하얀색 스위치를 눌러봅니다. 그 순간, 화장실 변기 물이 내려갑니다.

회식이 끝나고 집에 도착했습니다. 목이 말라 냉장고를 열어보니 콜라 페트병에 콜라 색 액체가 있습니다. 술기운에 별 생각 없이 벌컥벌컥 마셨는데 젠장, 간장이었습니다.

억지스러워 보이지만, 개발자들은 이런 경험을 종종 하게 됩니다.

개발자들은 항상 코드로 여행을 떠납니다. 특히 잘 모르는 코드로 여행을 자주 가게 됩니다. 남이 짠 코드, 라이브러리 코드는 물론이고 내가 옛날에 짠 코드도 마찬가지입니다. 여기서 말하는 옛날이란 정말 1주~1개월 정도의 정말 옛날일 수도 있지만, 사실 5분 전에 작성한 코드일 수도 있습니다.

그리고 남이 만든 함수에, 내가 만든 타입에, 내가 써놓은 주석에 종종 배신당합니다. 또는 배신당하지 않기 위해 많은 시간을 소비하게 됩니다.

  • findById 라는 함수가 있길래 이용했는데 원본 배열이 바뀌어 버려서 예상치 못한 곳에서 오류가 발생합니다.
  • closeModal 이라는 함수가 있길래 수행했는데 모달이 닫히는 것뿐만 아니라 다른 페이지로 이동하기까지 합니다.

도로를 설계하는 사람은 과속방지를 위해 안내판을 도로 곳곳에 놔두기도 하지만, 이를 넘어 과속단속 카메라를 설치하고, 구간단속 시설을 설치하고, 과속방지턱을 설치합니다. 하지 말라고 말하는 대신 하지 못하게 막아버리는 것입니다.

행사를 준비할 때는 사람들에게 당일에 이리 가세요 저리 가세요 라고 설명하는 대신 미리 바닥에 화살표를 붙여두고 가벽을 세워 이동 경로를 제한합니다. 그리고 사람들은 그 제한과 안내를 따라 어쩔 수 없이 가이드를 따르게 됩니다.

코드도 마찬가지입니다. 미래의 나와 내 동료를 위해, "알아서 잘 하겠지" 라고 생각하는 대신 내가 할 수 있는 모든 제한을 걸어야 합니다. 그리고 미래의 나는 그 제한을 따라 개발하면서 기존 코드에게 "친절하다" 라고 느낄 것입니다.

친절한 코드를 작성하려면 어떻게 할 수 있을까요?



몇 가지 기법들

이름을 못 짓지 않는다

이름을 잘 짓는 건 가장 기본적이면서도 가장 어려운 일입니다. 예약어도 피해야 하고, 다른 변수들과 겹쳐도 안 되고, 너무 짧아도 안 되고, 너무 길어도 안 되고, 단어가 너무 어려워도 안 되며, 그러면서도 역할이 훌륭하게 드러나야 합니다. 너무나도 어렵고 내 역량으로는 할 수 없을 것처럼 느껴집니다.

하지만 이름을 잘못 짓지 않는 건 생각보다 쉽습니다. 이름을 잘 짓는 건 역량으로 해석할 수 있지만, 이름을 못 짓는 건 책임감과 섬세함의 문제입니다. 배열에서 가장 큰 원소를 찾는 함수의 이름이 find 라면, 그냥 이름을 대충 지은 것입니다. 반대로 findBiggest 함수의 역할이 배열을 받아서 가장 큰 원소 세 개만 뽑아 배열 형태로 반환하는 거라면, 이건 아마도 처음에는 이름을 잘 지었으나 함수의 역할이 변하는 과정에서 섬세함이 부족해서 발생한 일일 것입니다.

  • 함수에 기능이 추가되었다면, 아마 이름도 같이 변경되어야 할 것입니다. 함수의 내용에 수정이 있었다면 이름이 변경되어야 하지 않는지 고려해야 합니다.
  • 이름을 지은 다음, 이름이 함수가 하는 행동을 충분히 표현하는지 고민해보면 좋습니다.

함수형 코드

이름을 잘 짓기 위해 함수형 기법을 적용하는 것이 도움이 되기도 합니다.

가령 아래 두 코드를 비교해 볼까요?

let sum = 0;

// 이 줄 sum 변수는 거짓말을 하고 있다.
// 누군가 이 코드를 처음 보고 "이름이 `sum` 이니까 합이겠지?" 라는 생각으로
// 이 줄에서 (=아래에 있는 forEach 문이 수행되기 전에)
// 이 변수를 가져다 쓴다면?

array.forEach(e => { 
  sum += e;
});

위 로직에서 이름에 큰 문제가 없어 보이지만, 사실 array 의 원소들을 sum 에 더하는 동안 (주석에 해당하는 곳) sum 변수는 거짓말을 하고 있습니다. forEach 문이 완료되기 전까지, sum 변수의 값은 array의 원소들의 합이 아닙니다. 이 정도 길이에서는 괜찮지만 추후 로직이 복잡하고 길어질 경우, 누군가 이 사실을 모르고 연산들이 모두 완료되기 전에 sum 변수를 가져다 쓸 수 있습니다.


const sum = array.reduce((a, c) => a + c, 0);

이렇게 작성하면 sum 변수는 선언되는 순간부터 원소들의 합이므로, 어느 시점에도 sum 변수의 값이 거짓말을 하지 않습니다.


만약 위처럼 함수형 함수를 사용하기 어려운 상황이라면,

const sum = (() => {
  let tempSum = 0;
  array.forEach(e => { 
    tempSum += e;
  });
  return tempSum;
})();

이렇게 IIFE를 활용하는 것이 도움을 줄 수도 있습니다.


용어를 통일한다

하나의 개념을 표현하는 데에는 여러 단어가 있습니다. 가령 "정렬" 이라는 기준을 표현하는 데에는 sortorder 두 가지 단어가 있는데요,

코드의 어느 부분에서는 정렬에 order 라는 단어를 쓰고 다른 곳에서는 sort 라는 단어를 쓰면, 개발자는 둘의 차이를 궁금해하게 됩니다. 그리고 그 끝에 둘에 아무런 차이가 없다는 것을 깨닫고 분노하게 됩니다.

용어를 통일해서 "이건 이것과 같은 내용이야"를 알려주는 게 좋습니다. 그러면 개발자는 앞선 경험을 토대로 동일한 경험이 있을 거라고 예상하며 코딩을 할 수 있어 생산성이 증가합니다.


변수명을 요란하게 짓는다

어떤 함수/변수는 사용할 때 주의해야 할 수 있습니다. 가령 프로젝트 전체에서 단 한 번만 사용되어야 하는 코드도 있고, 기능은 존재하지만 이용했을 시 보안상 이슈가 있어서 주의해야 하는 코드도 있고, 프론트엔드를 하다 보면 SSR에서 서버와 브라우저 모두에서 수행될 수 있는 코드를 작성하게 되는데 이때 서버에서는 수행되면 안되고 브라우저에서만 수행되어야 하는 컴포넌트를 만들 때도 있습니다.

이런 경우 변수명을 아주 요란하게 짓는 것이 도움이 될 수 있습니다. 가령 <div innerHtml={html} /> 보다 <div _dangerouselySetInnerHtml={html} /> 가 훨씬 위험해 보입니다. 기능을 이용하기 전에 한 번 더 생각하게 됩니다.

앞의 콜라 예시에서 보자면, 콜라병에 간장을 넣어놨다면 병에 포스트잇을 잔뜩 붙여두거나 병뚜껑에 씌워두는 등의 처리를 할 수 있겠습니다. 이를 통해 미래의 저는 간장을 마시기 전에 "엥?" 이라는 생각을 한 번 더 할 수 있습니다.


주석이라도 활용한다

로직에 영향이 없는 설명을 코드에 표현하는 방식 중 하나로 주석이 있습니다. 앞의 콜라페트병과 간장 예시에서는, 냉장고 문에 포스트잇을 붙여서 아래와 같이 써두는 것입니다.

콜라 페트병에 들어 있는 거 간장이다.

하지만 느껴지다시피 주석은 앞의 것들만큼 효과적인 방식은 아닌데요, 아래와 같은 문제점이 있습니다.

  • 주석은 잘 보이지 않습니다. 개발자가 주석을 읽으리라는 건 헛된 기대일 수 있습니다. 제가 술을 마시고 집에 들어와서 냉장고 문을 열고 간장을 마실 때 과연 냉장고 문에 붙어 있는 포스트잇을 봤을까요?
  • 주석은 outdate되기 쉽습니다. 개발자들은 코드를 업데이트하면서 주석을 같이 업데이트하지 않는 경향이 있습니다. 주석의 위치가 수정되는 코드의 위치와 거리가 먼 경우가 많기 때문에 자연스러운 상황입니다. 이렇게 outdate된 주석이 쌓이다 보면 다시 개발자들은 주석을 신뢰하지 않게 됩니다. 악순환입니다.
  • 보통 좋은 코드는 주석이 필요없습니다. 때문에 주석은 간혹 면죄부가 되어, 나쁜 코드를 합리화하는 수단의 역할을 하기도 합니다. 주석을 작성하기 전에 코드를 더 좋게 만들 수 없었는지, 주석이 필요없는 코드를 구현할 수는 없었는지 생각해보면 좋습니다.

그렇다고 주석이 꼭 나쁘다는 것은 아닙니다. 기획이 이렇게 된 것에 대한 노션 링크, 슬랙 논의 링크 등을 주석에 달아두는 것은 좋습니다.

또한 역량이나 시간의 문제로 나쁜 코드를 개선할 시간이 없어서 그대로 머지해야 할 때, 나중을 위해선 주석마저 없는 것보단 주석이라도 있는 게 좋습니다.


꽁꽁 싸매 숨긴다

사촌동생들이 집에 놀러 왔습니다. 소파에는 제가 아끼는 인형들이 엄청 많은데, 정말 많이 아끼는 것들이라 사촌동생들이 건드리지 않았으면 좋겠습니다. 이럴 때 우리에게는 사촌들에게 "건드리지 마" 라고 안내하거나, 건드리지 마라고 포스트잇을 붙여 둘 수 있겠지만, 사실 전부 다 숨겨버리는 것이 가장 좋습니다.

코드도 마찬가지입니다. 이것저것 안내하고 주석 달고 할 것 없이 그냥 숨겨버리는 게 좋을 수 있습니다.

가령 아래 코드는

const allTodos = await getTodos();
const completedTodos = allTodos.filter(t => t.isComplete);
const sortedTodos = completedTodos.sort(compareTodo);
const displayTodos = sortedTodos.slice(0, 5);

이름이나 이것저것 좋긴 한데, 변수가 너무 많습니다. 우리는 아래에 있는 모든 코드가 displayTodos 를 활용하길 바라지만, 미래의 나나 내 동료가 과연 그럴까요? 불필요한 것들을 은닉하고 필요한 것만 노출하면 미래의 내가 더 편할 것입니다.

const displayTodos = await (async () => {
  const allTodos = await getTodos();
  const completedTodos = allTodos.filter(t => t.isComplete);
  const sortedTodos = completedTodos.sort(compareTodo);
  return sortedTodos.slice(0, 5);
})()

테스트를 작성한다

테스트의 핵심은 아니지만, 테스트를 작성했을 때 아래 두 가지 장점이 존재합니다.

  • 동작을 보장하는 최소한의 안전장치가 되어 줍니다.
  • 동작을 설명하는 문서가 되어 줍니다.

따라서 테스트코드는 "어 이게 어떤 코드지? 동작하는 코드인가?" 라는 질문에 대한 가장 훌륭한 대답이 됩니다.

테스트코드를 잘 작성하면 미래의 나에게 큰 도움이 됩니다.


번외: 타입스크립트 타입

타입스크립트는 친절한 자바스크립트를 도와주는 도구입니다. 타입스크립트를 통해 자바스크립트 코드의 자유를 제한하고 "가능해야 하는 것만 가능하게" 막아둘 수 있습니다.

const [modalState, setModalState] = useState<
  | { isOpen: true; detailItemId: number; }
  | { isOpen: false; }
>({ isOpen: false })

이렇게 타입을 활용하면 미래의 내가 가면 안 되는 길을 갈 때 더 빠르게 깨달을 수 있습니다.



실천하기

미래의 나를 배려하는 마음가짐

사실 위의 기법들이, 우리가 몰라서 안 하는 것들은 아닙니다.우리 모두는 IIFE가 뭔지 알고 있고, 함수형 코드가 뭔지 알고 있고, 변수명을 지을 줄 알고, 주석을 달 줄 압니다.

위의 기법들이 특별히 많은 시간이 들어가지도 않습니다. 주석을 다는 데에 1분이 넘게 걸리는 사람은 없습니다.

중요한 건 마음가짐입니다. 당장 한두 시간 안에 개발 다 끝내고 아무도 다시는 안 볼 코드가 아니라면, 항상 "이거 나중의 내가 봤을 때 안 헷갈릴까?" 라는 고민을 하는 게 좋습니다.


더 나은 코드를 만들고 떠나기

아무리 신경써도, 언제나 놓쳐지는 것들은 있기 마련입니다. 변수명이 이상하거나, 주석이 outdate되었거나 등등의 상황을 종종 보게 될 수밖에 없습니다.

이럴 때 그냥 지나치기보단 이슈를 발견했을 때 뭐 하나라도 더 괜찮게 만들고 떠나는 습관이 도움이 됩니다. 청소를 했을 때 가장 높은 효과가 나오는 시기는 바로 지금입니다. 지금 안 고치면 앞으로도 안 고칩니다.

미국 보이스카우트 규칙처럼, 언제나 코드를 더 깨끗하게 만들어놓고 떠나는 문화가 있다면 코드는 점점 친절해질 것입니다.



결론

내가 작성하는 코드에 대해 가장 잘 아는 사람은 언제나 지금의 나입니다. 미래의 내가 더 잘 알 리가 없습니다. 미래의 나는 지금의 내가 짜둔 코드로 여행을 오는 손님입니다.

빨리 기능을 구현해야 하는데 미래를 대비하느라 시간을 낭비한다고 생각할 수 있지만, 이는 잘못된 생각일 수 있습니다. 여기서 말하는 미래는 그리 먼 미래가 아닙니다. 실제로 5분 전에 구현한 코드를 이해하지 못해서 시간을 날리는 경우도 비일비재합니다. 처음부터 친절하게 작성하면 결국 더 빨리 구현할 수 있습니다.

시간 낭비나 불필요한 일이 아닙니다. 등산로에 울타리를 설치하는 일이고, 화장실 콘센트에 물 방지 덮개를 설치하는 일이고, 현관문에 도어락을 설치하는 일입니다.

불쌍한 미래의 나를 위해 먼지 쌓인 멀티탭은 줄을 잘라버리고, 간장이 들어있는 콜라병은 종이컵을 씌우고, 보일러가 안 들어오는 방은 들어가지 못하게 문을 잠궈두는 건 어떨까요?

profile
프론트엔드 개발자입니다
post-custom-banner

7개의 댓글

comment-user-thumbnail
2024년 2월 9일

항상 많이 배우고 있습니다.
감사합니다 :)

1개의 답글
comment-user-thumbnail
2024년 2월 9일

무엇이든 기본이 제일 중요한데 쉬운 언어와 재미난 예시로 한번 더 상기 해줄 수 있는 좋은 글이에요! 고맙습니다 :)

1개의 답글
comment-user-thumbnail
2024년 2월 10일

Sum 코드는 예제가 자바스크립트라는 가정 하에, 조금 결함이 있어 보입니다.

Array.forEach 함수는 블로킹 방식입니다. 함수 동작 중, sum 변수를 읽을 여지가 없습니다. 자바스크립트는 멀티스레딩을 사용하는 것도 아니고요. 제가 아는 한, 그러므로, 거짓말이라고 보기 어렵습니다. 물론 글의 의도는 무엇인지 잘 접수되긴 했지만, 조금은 껄끄럽게 전달된다는 게 문제겠네요.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach

"forEach()는 동기 함수를 기대하며 프로미스를 기다리지 않습니다. 프로미스(또는 비동기 함수)를 forEach 콜백으로 사용할 때는 그 의미를 알고 있어야 합니다."

1개의 답글
comment-user-thumbnail
2024년 2월 22일

좋은 글 잘 읽었습니다 감사합니다.
좋은 하루 보내세요~

답글 달기