좋은 코드란 어떤 코드일까

date
Oct 1, 2023
thumbnail
slug
좋은 코드란 어떤 코드일까
author
status
Published
tags
TypeScript
JavaScript
WEB
summary
최근 시니어 개발자분과 이야기를 나누면서 좋은 코드란 어떤 코드인가에 대해 고민해보게 되었다. 개발을 하면서 고민했던 포인트들이 머리 속으로 정리된 것 같아 글로 기록해보려한다.
type
Post
updatedAt
Jan 21, 2024 07:17 AM

시작하며

최근 시니어 개발자분과 이야기를 나누면서 좋은 코드란 어떤 코드인가에 대해 고민해보게 되었다.
개발하다보니 고민했던 포인트들이 머리 속으로 정리된 것 같아 글로 기록해보려 한다.

좋은 코드란?

좋은 코드란 무엇일까? 주변 개발자들에게 물어보니 아래 세가지 답변으로 좁혀졌다.
  • 가독성이 좋은 코드
  • 재사용성이 높은 코드
  • 적절하게 추상화되어 있는 코드
나온 키워드는 가독성, 재사용성, 추상화인데 스스로 고개를 끄덕이고 있음에도 조금만 생각해보면 사람마다 주관적으로 다가올 수 있다.
 
나는 위 세 가지의 답변을 “관심사” 라는 하나의 키워드로 좁힐 수 있다고 생각하는데 관심사의 관점은 별개의 글에서 다뤄보려고 한다.

가독성이 좋은 코드?

💡
Question
  • 가독성은 주관적인데 객관적으로 읽기 좋은 코드는 무엇일까?
  • 읽기 편함 == 가독성이 좋음 == 좋은 코드?
재엽님의 글 중 좋은 코드란 무엇일까? > 읽기 쉬우려면 주석을 쓰면 되지 않나? 에서 설명해주신 예시를 많이 참고했다.
가독성이라는 단어는 매우 주관적이다. 사용자의 상황이나 관점에 따라서도 달라질 수 있다.
가볍게 생각해봐도 한국인이라면 한글이 잘 읽힐 수 있지 않을까?
 
먼저 블로그를 개발한다고 가정해보자.
아래 코드는 포스트를 보여주는 카드 형태의 컴포넌트인데 주석으로 해당 코드를 설명하고 있다.
// A: 포스트 카드
// b: 제목 날짜 이미지를 받는 인자
// b.c: 이미지
// b.d: 제목
// b.e: 날짜
const A = (b) => {
	return(
		<Container>
			<StyledPostImage src={b.c}/>
			<StyledTitle>{b.d}</span>
			<StyledDate>{b.e}</date>
		</Container>
	);
}
가장 눈에 띄는 부분은 주석을 사용했다는 점이다. 컴포넌트의 역할은 블로그 카드이지만 A라는 컴포넌트 이름을 할당했고 사용하는 사람의 입장에서는 주석까지 확인해야하는 불상사가 발생한다.
 

컴포넌트나 인자들의 명확한 명칭을 정하자

const PostCard = (props) => {
	return(
		<Container>
			<StyledPostImage src={b.postImage}/>
			<StyledTitle>{b.postTitle}</span>
			<StyledDate>{b.postUpdateDate}</date>
		</Container>
	);
}
그럼 이렇게 개선해보면 어떨까? 컴포넌트의 이름을 PostCard로 명확하게 명시했고 props도 직관적인 이름으로 변경했다. (인자들의 정보는 타입스크립트를 쓰면 해결될 것 같다.)
 
형태나 역할, 기능에 따라 컴포넌트의 이름을 정한다면 객관적으로 가독성이 좋은 코드라고 할 수 있겠다.

재사용성이 높은 코드?

💡
Question
  • 재사용되는 절대적인 횟수가 높으면 좋은 코드일까?
위에서 만든 포스트 컴포넌트를 여러 페이지에서 사용하게 되었다. 박수~👏
이 컴포넌트는 반복적으로 사용되어 재사용성이 높다는 기준을 충족하게 되었다.
과연 이게 좋은 코드라고 말할 수 있을까? 🤔
const PostCard = (props) => {
	return(
		<Container>
			<StyledPostImage src={props.postImage}/>
			<StyledTitle>{props.postTitle}</span>
			<StyledDate>{props.postUpdateDate}</date>
		</Container>
	);
}
이번에는 새로운 요구사항이 생겼다고 가정해보자.
블로그를 모아놓은 블로그 리스트 페이지를 만들어야하는데 포스트 카드와 형태가 매우 비슷하다.
하지만 포스트 카드는 포스트 데이터에만 대응하고 있다.
새로 만든다면 비슷한 형태가 중복되는데 괜찮을까?

도메인 로직을 제거해보자.

포스트카드에서 도메인인 포스트만 제거하면 어떨까?
같은 UI를 사용하면서도 데이터에 유연하게 대처할 수 있는 컴포넌트를 만들어보자.
const TopImageCard = ({image, title, date}) => {
	return(
		<Container>
			<StyledImage src={image}/>
			<StyledTitle>{title}</span>
			<StyledDate>{date}</date>
		</Container>
	);
}
PostCard 대신 TopImageCard상단에 이미지가 있는 형태를 명시해주었다.
내부 props도 포스트 데이터에 의존하지 않도록 가장 기본이 되는 데이터로 변경해주었다.

도메인 로직을 구분하는 기준은 뭐지?

그럼 도메인은 뭘까?
Domain: 통치자나 정부가 소유하거나 통제하는 영토 지역
통제하는 영역, 지역 라는 뜻을 가지고 있는데 이걸 우리가 개발했던 컴포넌트들에 적용해보자.
포스트 컴포넌트, 블로그 카드 컴포넌트 에서는 포스트라는 영역, 블로그라는 영역, 카드라는 영역이 도메인이 될 수 있다.
컴포넌트의 역할인 카드를 제외한 나머지 도메인을 제거한다면 도메인 로직을 분리한다고 볼 수 있겠다.

확장성 있는 코드

다른 상황을 가정해보자. Card 내부에 들어갈 데이터를 변경해야한다.
const TopImageCard = ({image, title, date}) => {
	return(
		<Container>
			<StyledImage src={image}/>
			<StyledTitle>{title}</span>
			<StyledDate>{date}</date>
		</Container>
	);
}
단순하게 props를 하나 추가하는건 어떨까? 가장 빠르고 쉽게 대처할 수 있는 방법이다.
const TopImageCard = ({image, title, date, content, author}) => {
만약 커스텀 해야할 데이터가 많아진다면 이 컴포넌트는 흔히 말하는 Monster 컴포넌트가 될 것이다.
const TopImageCard = ({contents}) => {
	return(
		<Container>
			<StyledImage src={image}/>
			{contents}
		</Container>
	);
}
이번에는 이렇게 컨텐츠 영역을 외부에서 컨트롤 할 수 있도록 children으로 내리도록 변경해봤다.
외부에서의 데이터의 변경이나 스타일 변경에도 유연하게 대처할 수 있는 코드가 되었다.
이정도 레벨은 디자인 시스템에 가까운 것 같기도 하다

숨겨야할 코드와 드러내야할 코드

💡
Question
  • 숨겨야할 코드와 드러내야할 코드는 어떤 코드일까?
디자인 시스템을 개발하며 많이 고민했던 부분이다.
2가지 예시를 살펴보자.

형태를 분리하는 경우

const TopImageCard = ({contents}) => {
	return(
		<Container>
			<StyledImage src={image}/>
			{contents}
		</Container>
	);
}
위에서 사용했던 코드를 그대로 들고 왔다. 이 컴포넌트는 컨테이너 상단에 이미지가 있고 하단에 contents를 받을 수 있는 간단한 컴포넌트이다. 이미지의 스타일까지 커스텀이 필요한 상황이라면 아래와 같이 작성해 볼 수 있겠다.
const VerticalContentCard = ({header, contents}) => {
	return(
		<Container>
			{header}
			{contents}
		</Container>
	);
}
첫 번째 예시인 TopImageCard는 이름으로 상단에 이미지가 있는 컴포넌트의 역할임을 추측할 수 있고 두 번째 예시인 VerticalContentCard는 이름으로 새로로 나열되는 컴포넌트의 역할임을 추측할 수 있다.
각 컴포넌트가 맡은 역할에 따라 컴포넌트의 이름과 추상화된 부분이 변경될 수 있다.

기능을 분리하는 경우

토글 기능을 여러 곳에서 사용할 경우 사용할 목적으로 토클 기능을 추상화하려고 한다.
export const useOnToggle = (selected, defaultSelected) => {
 //... 생략
  return [isToggle, handleToggle];
};
useOnToggle은 토글 기능만을 위한 훅으로 토글을 제외한 기능은 감추고 외부 상태에 대한 확장성을 가지고 있다.
const [isToggled, handleToggle] = useOnToggle(isSelected, defaultSelected);
이렇게 컴포넌트가 받은 역할에 대해서는 추상화하고 다른 부분은 드러내는 것으로 확장성을 가질 수 있다.
 
이렇게 2가지 예시를 보았는데 VerticalContentCarduseOnToggle의 공통점을 정리해보자.
첫 번째 예시인 VerticalContentCard는 시각적인 형태의 역할을 부여받았다. 따라서 형태를 제외한 나머지 로직은 포함하지 않음으로서 확장성을 가져가고 있다.
두 번째 예시인 useOnToggle는 토글이라는 기능의 역할을 부여받았다. 따라서 토글 기능을 제외한 나머지 로직을 포함하지 않으므로서 확장성을 가져가고 있다.
 
코드의 역할을 담고있는 핵심 로직(개발자가 몰라도 되는 세부구현) 숨기고 이외의 로직은 드러냄으로서 확장성을 가져갈 수 있다.

적절하게 추상화되어 있는 코드는?

적절하게 라는 말도 추상적인데 추상화라는 단어도 추상적이다.
글로 정리하다보니 머리가 조금씩 아파오는데 내 경험에 따르면 상황에 따라 변할 수 있다고 생각한다.
카드 내부에 들어가는 이미지의 스타일이 고정되어 있다는 약속을 했다면 전자, 내부 요소의 커스텀이 자주 일어난다면 후자가 더 나은 선택일 수도 있겠다.
각자 상황에 알맞게 선택하는게 적절한 추상화가 아닐까?
 

정리하면

각자 상황에 맞는 추상화를 설정하자.

  • “적절한” 추상화는 개발자의 몫

코드의 역할을 명확하게 설정하자.

추상화를 진행하기 전 다시 한번 “왜? 이걸 추상화 해야하는지, 어떤 역할인지”를 고민해보자.
  • 역할이 명확해진다면 변수나 함수의 이름도 명확해진다.
  • 역할이 명확해진다면 핵심 코드도 명확해진다.
  • 추상화된 코드가 명확한 역할을 한다면 활용도도 그만큼 높아지게 된다.