개발자들은 간혹 "대충 짜면 더 빠르다" 또는 "일단 엉망으로 만들어두고 나중에 고친다", "바쁘니까 테스트코드는 패스한다" 와 같은 이야기를 하곤 합니다.
과연 엉망진창으로 구현하면 더 빠를까요?
formatDate
함수를 망쳐보자지금부터 개발자 A가 formatDate
라는 평범한 함수를 처음 만들고 이후로 서서히 망치는 과정을 쫓아가 보겠습니다.
투두 리스트 피쳐가 나왔고, 아직 API 개발이 완료되지 않아 프론트에서 마크업 먼저 작업하기로 했습니다. 날짜를 보여줘야 했기 때문에 이렇게 @/utils/formatDate.ts
파일에 formatDate
함수를 만들었습니다.
export const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return [
date.getFullYear().toString(),
(date.getMonth() + 1).toString().padStart(2, '0'),
date.getDate().toString().padStart(2, '0'),
].join('-');
};
이 함수는 string
형태의 문자열 받아서 YYYY-MM-DD
형태로 포매팅해서 반환합니다. 네이밍을 formatDateStringToYYYYMMDD
라고 하거나, 아니면 포맷을 파라미터로 받을 수 있게 (date: string, format: 'YYYY-MM-DD') => string
정도 타입이었다면 더 명확했을 수도 있겠지만, 이 정도는 취향 차이라고 볼 만 하고 충분히 괜찮은 코드입니다. 인풋으로 받은 dateStr가 invalid한 경우에 대한 핸들이 없는 게 조금 아쉽지만, 까먹었나 봅니다.
아직 API 연결을 안 한 상황이었기에, 이 함수는 목 데이터를 통해 이용되고 있었습니다.
const mockDate = '2023-12-12';
return (
<div>
<p>날짜</p>
<p>{formatDate(mockDate)}</p>
</div>
);
뭐 나쁘지 않습니다.
백엔드 개발이 완료되었다고 해서 API를 붙였습니다. 스펙은 아래와 같습니다.
GET /todo
{
title: string;
createDate: string;
}
const { data } = useQuery({ /* ... */ });
return (
<div>
<p>투두 생성 날짜</p>
<p>{formatDate(data?.createDate)}</p> {/* TSError: 🚨 string | undefined 형식을 string 형식에 에 할당할 수 없습니다. */}
</div>
);
아뿔싸, data 가 undefined
일 수 있단 걸 까먹었습니다. 우선 잘 동작하는지 확인부터 하고 싶고, 마침 에디터에 formatDate
함수가 켜져 있어서 신택스를 이렇게 변경합니다.
이제 문제없이 동작합니다. 개발자 A는 이대로 가기로 합니다.
QA 환경에서 간혹 인터넷이 느린 상황이 있었고, 이때 투두 아이템이 비어있는 게 어색해 보였던 QA팀에서 UX 개선을 제안합니다.
🙋 투두 아이템이 로딩중일 때 각 셀에
'...'
이라는 텍스트를 보여주세요
마침 지금 코드에 적절한 자리가 있어서, 개발자 A는 코드를 이렇게 변경합니다.
많은 일들이 있었습니다. 신규 피쳐가 세 개 정도 더 나왔고, 새로운 아이폰이 출시되었고, 연봉도 5% 정도 올랐고, 즐겨 보던 웹툰이 완결했습니다. 이런 시간이 지나는 동안 잘 만들어진 formatDate
함수는 10개의 새로운 파일에서 재사용되었습니다.
기획이 변경되어서, 사용자가 생성하지 않은 투두가 사용자 투두 목록에 존재할 수 있게 되었습니다. 이런 상황일 경우, 투두를 보여주는 화면인 "투두 목록" 과 "투두 상세조회" 두 화면에서 투두의 생성 날짜 란에 자동 생성됨
이라는 문자열을 보여줘야 하게 되었습니다.
기획이 변경되었으니 API 스펙도 아래와 같이 변경되었습니다.
GET /todo
{
title: string;
createDate: string | null;
}
시계를 보니 2시 58분이고, 3시에는 회의가 있습니다. 이 부분 대응을 해 주려면 createTime
이 null
인지 검사해서 분기치는 로직을 두 군데 (투두 목록 & 투두 조회 페이지)에 구현해줘야 합니다. 두 군데 모두에 구현하자니 중복 로직인 것 같아서 고민이 되는데 고민할 시간이 없습니다.
급한대로 페이지가 아닌 formatDate
함수를 수정해서 이렇게 메꿨습니다. 어차피 dateStr이 null
인 경우는 투두의 생성 시간밖에 없으니 다른 곳에 영향은 없을 것입니다.
잘 동작하는 걸 확인했고, PR을 올리고 머지하고 회의하러 후다닥 나가봅니다.
몇 달이 지났고, 신규 피쳐가 들어옵니다. 이번에는 투두가 아니라 유저의 createTime
이 null
일 수 있게 되었습니다. 그리고 이 경우에는 유저 테이블의 가입 날짜
컬럼에 -
라는 문자열을 보여줘야 합니다.
슬슬 formatDate
가 뭐하는 함수인지 기억이 잘 나지 않습니다. 기존 formatDate
함수를 보니 뭔 뜬금없는 '자동 생성됨'
이라는 문자열을 반환하고 있습니다. 깃 로그를 보니 분명 내 코드인데, 무슨 코드인지 모르겠습니다. 30분에 걸쳐 사용처를 일일이 추적해봤지만, 과거의 내가 저렇게 구현한 이유가 있지 않을까? 싶어서 고치기는 꺼려집니다.
불안해서 리팩토링은 못하겠고 임시방편으로 기존 기능들을 고장내지 않도록 이렇게 수정해 봅니다.
최종 코드는 아래와 같습니다.
export const formatDate = (
dateStr: string | undefined | null,
isUserPage?: boolean
) => {
if (dateStr === undefined) return '...';
if (dateStr === null)
return isUserPage ? '-' : '자동 생성됨';
const date = new Date(dateStr);
return [
date.getFullYear().toString(),
(date.getMonth() + 1).toString().padStart(2, '0'),
date.getDate().toString().padStart(2, '0'),
].join('-');
};
충분히 망가진 것 같습니다. 이 함수를 위한 가장 적절한 이름은 formatDateStringReturnDotsIfUndefinedAndReturn자동생성됨IfNullButReturnHyphenIfUserPage
정도가 되겠네요. 누구도 리팩토링하기 싫고 누구도 재사용하고 싶지 않은 코드가 탄생했습니다.
formatDate
함수는 왜 망가졌을까?위 예시는 분명 억지스러운 면도 있고, 사실 결과적으로 그렇게 많이 망가지지도 않았습니다. 몇 시간 정도를 들여 충분히 복구할 수 있는 수준입니다.
하지만, 분명 이 글을 여기까지 읽은 분들이라면 제가 무슨 말을 하는지 이해하셨을 것입니다. 우리 모두는 이런 경험이 있고, 감당하지 못할 괴물을 만들어봤고, 그 괴물을 해치워 보겠다고 사용처를 일일이 추적하며 시간을 쏟았고 결국 패배해 보기도 했습니다.
개발자 A에게는 formatDate
함수를 정상적으로 만들 기회가 분명 있었습니다. 그리고 그 과정에서 시간이 더 많이 들지 않았을 것입니다. 하지만 개발자 A는 그럴 때마다 외부의 요인이든 내부의 요인이든 이 함수를 고치지 않는 선택을 했고, 결국 그 선택들이 시간이 흐르며 점점 누적되어 함수를 망가뜨리게 되었습니다.
개발자 A는 코드를 엉망으로 만듦으로써 많은 시간을 잃었습니다. 사용처를 추적해야 했고, 디버깅에 시간을 쏟아야 했고, 리팩토링을 시도하다가 망하기도 했습니다.
개발자 A에게 있었던 formatDate 함수를 살릴 수 있는 많은 기회들에서 조금만 더 고민했었더라면, formatDate 함수는 망가지지 않았을 것입니다. 그리고 이 고민의 시간은 전부 다 합쳐 봐야 10분 정도밖에 되지 않았을 것입니다. 겨우 10분 정도 아끼고 몇 시간을 날린 셈입니다. 깨진 유리창의 법칙처럼, 망가진 코드는 그 망가진 수준이 누적되기에 지금 고치지 않는다면 앞으로도 더 망가지고 결국 더 많은 시간을 날릴 것입니다.
코드를 엉망진창으로 구현하면, 누구에게도 단 하나도 좋지 않습니다.
코드가 엉망진창이 되지 않도록 지켜주려면 어떻게 하면 될까요?
우리가 구현하는 거지만, 코드는 제멋대로 성장하는 아이와도 같습니다. 어느 날 봤더니 파라미터가 하나 추가되어 있고, 어느 날에는 뜬금없는 로직이 들어가 있습니다.
코드에 지속적으로 관심을 가지고, 잘못된 길로 접어들었을 때 가능한 빨리 되돌려놓는 것이 중요합니다.
TDD를 하는 것도 도움이 됩니다. 테스트를 짜면 내가 구현해야 하는 로직이 뭔지 명확해지는 것뿐만 아니라, 인터페이스 설계를 두 번 이상 하게 되기 때문에 "야 잠시만, 이게 맞아?" 와 같은 질문을 스스로에게 할 수 있는 확률이 높아집니다.
뭔가 잘못됐을 때 빠르게 고치는 것도 중요하지만, 애초에 코드가 잘못되지 않는다면 그게 바로 가장 빠르게 구현하는 길일 것입니다. 어차피 미래의 상황은 예측할 수 없기 때문에, 좋은 코드는 확장에 열려 있어야 합니다. 그러기 위해서는 더 확장에 열려 있고 변경하기 쉬운 유연한 코드를 짜는 기법에 대한 연습과 경험이 필요하고, 이를 통해 개발자로서 성장해야 합니다.
간혹 "제대로 구현한다" 라는 말이 엄청난 추상화나 대단한 인터페이스를 의미한다는 오해가 생기곤 합니다. 그래서 파일을 분리해야 할 것 같고, 코드를 쪼개야 할 것 같고, 귀찮고 멀게만 느껴집니다.
그렇지 않습니다. 코드는 필요한 만큼만 쪼개져 있어야 합니다. 오히려 formatDate 함수를 만들지 않고 한 함수 안에 모든 로직을 다 집어넣는 게 답일 수도 있습니다. 우선은 이 함수를 파일을 분리하지 않고 컴포넌트 파일의 하단에 스코프만 나눠서 그냥 작성하는 것도 좋은 해결책일 수 있습니다.
일반적으로 과하거나 잘못된 추상화는 코드를 망가뜨리는 주범입니다.
무관심과 안일함, 그리고 조금의 역량 부족이 합쳐져서 엉망진창인 코드가 탄생합니다. 그리고 이런 코드는 나와 내 팀원 모두를 괴롭히는 괴물이 되곤 합니다.
엉망진창으로 짜지 않기 위한 노력은 귀찮을지언정 생각보다 시간이 많이 들어가지는 않습니다. 더 빨리 찾고 대응할수록 더 적은 시간과 노력이 소요됩니다.
코드가 엉망진창이 되지 않기 위해, 코드에 지속적인 관심을 가지고 개선하려 노력해야 합니다. 코드가 잘못된 길을 가고 있는 것 같다면, 발견하는 즉시 고쳐야 합니다. 또한 처음부터 코드가 잘못되어질 확률을 낮추기 위해 유연한 코드를 작성할 수 있는 능력을 기르는 것이 좋습니다.