Posts 좋은 코드란 무엇인가?
Post
Cancel

좋은 코드란 무엇인가?

좋은 코드란 무엇일까?

일반적으로 좋은 코드라고 하면 다음 세가지를 얘기한다.

  • 읽기 좋은 코드
  • 테스트가 용이한 코드
  • 중복이 없는 코드

그럼 왜 위 세가지가 갖춰진 코드를 좋은 코드라 하는 걸까? 그리고 어떻게 이 세가지를 지키며 코드를 작성할수 있을까?

왜 좋지 않은 코드가 생산되는가?

우리는 항상 좋은 코드를 작성하기 위해 노력한다.

하지만 언제나 이것을 놓치는 시점이 존재하기에 좋지 않은 코드가 나오게 된다.

그럼 어떠다가 놓치게 되는 것일까?

우리가 마주치는 상황

  • 이미 운영중인 서비스의 CS 대응하며 수정
  • 변경된 요구사항, 인터페이스를 반영
  • 어제 또는 방금 작성한 코드 수정

우리는 언제나 코드를 수정하고, 다시 만든다. 그러는 과정에서 다음과 같은 상황이 발생하게 된다.

쓰이지 않는 코드

수정을 반복하면서 쓰이지 않는 코드가 발생하게 된다. 하지만 우리는 보통 이 코드를 지우지 않고 놔둔다. 바로 어디서 쓰일지 모른다는 불안감 때문이다.

이 불안감은 어디서 오는 것일까?

  1. 거리

    코드가 동작하는 곳과 코드가 정의된 곳 사이의 물리적 거리가 멀면 파악하기 힘들어진다. 보고 있는 코드가 일정 범위에서만 사용된다면, 그 범위만 확인 후 사용하지 않는 코드라는 것을 확신할 수 있을것이다.

  2. 순수하지 않은 함수

    함수 외부의 어떤 값을 기반으로 동작하는 함수는 그 side effect를 제어하기 어렵다. 함수의 입출력만 확인하여 함수 내부를 수정할 수 없다면 수정하기 까다롭다.

응급처치한 코드

중복되어서 하나의 모듈로 따로 빼두었지만, 쓰이는 곳에 따라 다르게 동작하도록 로직을 추가해야할 때를 마주친다.

이때 보통 side effect가 두려워서 함수에 입력을 추가하든가, 옵션 값을 추가하여 내부에서 억지로 처리한다. 이때 이 코드는 좋지 않은 코드가 된다.

이런 응급처치가 여러번 반복되면 아무도 알아볼수없는 함수가 탄생한다.

기술부채

물론 응급처치가 필요할때가 존재한다. 급하게 문제를 해결해야할때는 응급처치가 필요하다. 하지만, 언제나 이것을 부채처럼 여기고 수정해야한다.

좋지 않은 코드 줄이기

좋은 코드란 좋지 않은 코드가 없는 코드를 말한다. 따라서 좋지 않은 코드를 줄이고 격리하자. 이렇게 코드를 점진적으로 개선해나가야 한다.

추출이 아닌 추상화

추출

별다른 기준없이 중복 코드를 단순히 밖으로 빼내는 것

추상화

대상의 중요 요점들을 재해석해서, 의존성이 있는 것들끼리 묶어 밖으로 빼낸 것

의존성을 드러내기 위한 추상화

단순히 중복된 코드를 추출하는 것만으로는 재사용하기가 어렵다. 재사용을 위해 또다른 비용을 소모하게 될 뿐이다.

함수를 분리할때는 그 함수의 역할을 인지하고 하나의 역할만 하도록 정의해야한다. 즉, 의존성을 드러내어 의존성이 있는 것들끼리만 묶어서 함수의 추상화를 해야한다.

EX : before
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const Spagetti = () => {
  const [page, setPage] = useState(1);
  const [totalPage, setTotalPage] = useState(10);
  const [data, setData] = useState(null);
  const [isLoading, setLoading] = useState(false);

  const handlePage = () => {};
  const fetchData = async () => {
    fetch(page)
  };

  useEffect(() => {
    // data fetch
  }, [])

  return (
    <Component> ... </Componnet>
  )
}
  • pagination을 관리하는 state - page, totalpage 와 data의 상태를 관리하는 state - data, isLoading 이 다른 곳에서 동일하게 사용된다고 가정해보자.
  • 그대로 추출하면 다음과 같이 된다.
EX : after - bad
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function useSpagettiData(initialPage: number, fetchFunction: () => Promise<Data>) {
  const [page, setPage] = useState(1);
  const [totalPage, setTotalPage] = useState(10);
  const [data, setData] = useState(null);
  const [isLoading, setLoading] = useState(false);

  const fetchData = async () => {
    return await fetch(page);
  };

  const handlePage = () => { ... };

  useEffect(() => {
    // data fetch
  }, [])

  return {
    page, data, isLoading, isError, handlePage, ...
  }
}

const Spagetti = () => {
  const { data } = useSpagettiData(1, fetchData)

  return ( ... );
}
  • 단순히 추출을 한 결과이다.
  • 만약 코드에 변경이 생겨서 data의 초깃값을 설정해줘야한다면? api 응답값에 따라 total page가 달라지는 경우가 있다면? 아마 세번째 인자로 무언가를 추가하여 내부에서 분기를 추가하고, 로직을 재구성할 것이다.
  • 즉 한 함수안에 의존성이 서로 없는 부분이 섞여, 한 부분과 상관이 없는 부분이 변경되어도 함수 전체가 변경되어야한다.
  • 그럴 경우 스파게티 코드가 탄생하게 된다.
EX : after - good
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// pagination을 관리하는 hooks
function usePagination(initialPage: number) {
  const [page, setPage] = useState(initialPage);
  const [totalPage, setTotalPage] = useState(10);

  const handlePage = () => {};

  return { page, totalPage, handlePage };
}

// data fetch를 관리하는 hooks
function useFetch<T>(fetchFunction: () => Promise<T>, intialValue = null) {
  const [data, setData] = useState(intialValue);
  const [isLoading, setLoading] = useState(false);

  useEffect(() => {
    // data fetch
  }, [])

  return { data, isLoading, isError };
}

const Spagetti = () => {
  const { page, totalPage, handlePage } = usePagination(1);
  const { data } = useFetch(() => fetch(page), intialDadtdad);

  return ( ... );
}
  • 함수에 수정이 생겨도 의존성이 있는 부분끼리만 묶여있기에, 서로의 영역을 침범하지 않는다.

Where

그럼 어디에 분리할 것인가?

단순히 컴포넌트를 렌더링하는 함수 컴포넌트일 경우 어느정도 레벨로 공유될지에 따라 정의되는 위치가 달라진다. 어떤 함수는 도메인과 강하게 결합되어있어 특정 페이지에서만 재사용되고, 어떤 함수는 의존 관계없이 어디에서든 사용할 수 있다.

목적에 따라 맞는 위치를 정의해주는 것 또한 중요하다

삭제하기 쉬운 코드와 삭제하기 어려운 코드의 분리

아무리 깔끔하게 코드를 작성한다 하더라도, 요구사항에 맞추다보면 어쩔 수 없이 복잡한 코드가 작성된다. 사용하고 있는 라이브러리의 문제일수도 있고, 여러 브라우저에 대응하다 탄생한 코드일 수 있다.

이러한 좋지 않지만, 어쩔 수 없는 코드는 제대로 관리할 수 있도록 격리해줘야한다.

이때 주석과 함께 격리해두면 기존 코드의 흐름을 끊지 않고, 복잡한 코드를 이해하는데 도움을 줄 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
/**
 * @Note event type이 언제든 서버에서 추가될 수 있다. 하드코딩되어 있는 event name에 추가할 수 있도록 한다.
 */
const targetEventNameRegex =
  /click_|impression_|push_|scroll_|swipe_|background_/g;
/**
 * @Note eventName에 이전에 선택된 eventType_이 prefix로 붙어서 전달되어 이를 subtract 해준다.
 */
useEffect(() => {
  const refinedEventName = eventName?.replace(targetEventNameRegex, "");
  setValue(getName("eventName"), `${eventType}_${refinedEventName ?? ""}`);
}, []);

일관성있는 코드

최소한의 가독성을 보장하는 방법은 일관성있는 코드를 작성하는 것이다.

코드에 일관성이 지켜지면 예측이 가능해진다. 예측이 가능하다는 것은 어느 곳에 어떤 코드가 위치하는지 예상할 수 있다는 것이다. 이것이 프로젝트 팀원 간의 그라운드 룰이 필요한 이유이다.

Naming

변수 및 함수의 네이밍에 규칙을 만들자.

1
2
3
4
5
6
7
8
9
10
11
// Case 1.
// prefix: handle
// target: button
// action: click
const handleButtonClick = () => {};

// Case 2.
// prefix: on
// action: click
// target: button
const onClickButton = () => {};

Directory

우선 디렉터리 구조는 아키텍쳐가 아니다. (아키텍처 !== 디렉터리 구조)

일관된 디렉터리 구조는 전체적인 구조를 파악하는데 큰 도움을 주고, 컴포넌트간의 관계를 파악하는데에도 도움을 준다.

1
2
3
4
5
6
7
8
9
src
├── api
├── components
├── hooks
├── model
├── pages
│   ├── contract
│   └── docs
└── utils
  • components 는 컴포넌트를 모아둔 것, pages는 route path를 드러낸 곳이라는 것을 예측할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
src
├── @shared
│   ├── components
│   ├── hooks
│   ├── models
│   └── utils
├── contract
│   ├── components
│   ├── hooks
│   ├── models
│   ├── utils
│   └── index.ts
├── docs
│   ├── components
│   ├── hooks
│   ├── models
│   ├── utils
│   └── index.ts
└── App.tsx

첫번째 구조 : 코드가 하는 역할에 따라 디렉터리를 구분함. 어플리케이션이 커지면, 코드가 정의된 곳과 사용되는 곳이 멀어지는 문제점이 발생함.

두번째 구조 : 도메인 영역에 따라 디렉터리를 구분한 Featured Based 구조. 전반적으로 사용되는 것만 @shared에 넣고, 나머지는 연관이 있는 것끼리만 묶어놔서, 어플리케이션이 커져도 응집도 있게 모여있다.

참고자료

지역성의 원칙을 고려한 패키지구조

요약 : 프로그래머의 머리를 캐시라고 한다면, temporal locality, spatial locality에 의해 연관이 있는 기능끼리 모아두어 관리하면, 필요한 부분만 머리속에 넣어두어 사용할 수 있으므로, 빠르게 처리 가능하다.

확장성있는 코드

확장이 어려운 코드는 내부에서 많은 변경이 발생하며, 이것은 코드를 읽기 어렵게 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
const Input = styled.input`
  // styling
`;
const Input = ({
  type,
  value,
  onChange,
}) => {
  return (
    <Input type={type} value={value} onChange={onChange}>
  )
}

위 컴포넌트는 확장에 닫혀있다. 즉, 새로운 이벤트 핸들러나 값을 전달하기 어려운 구조이다.

따라서 아래와 같이 확장이 가능하도록 만들어야한다.

1
2
3
4
5
6
7
8
9
const Input = styled.input`
  // styling
`;

const Input = (props: HTMLAttributes<HTMLInputElement>) => {
  return (
    <Input {...props}>
  )
}

여기에 아래와 같이 validator에 대한 처리를 추가하면 좀 더 의미있어진다.

1
2
3
4
5
6
7
8
9
10
11
12
const Input = styled.input<{ isValid?: boolean }>`
  // styling
  // error styling
`;
interface Props extends HTMLAttributes<HTMLInputElement> {
  isValid?: boolean;
}
const Input = ({ isValid, ...props }: Props) => {
  return (
    <Input {...props} isValid={isValid}>
  )
}

출처

https://jbee.io/etc/what-is-good-code/

This post is licensed under CC BY 4.0 by the author.

css 선택자

React v18 주요 변경점

Comments powered by Disqus.