지나가던 개발(zigae)

프론트엔드 클린코드 - 변수 재할당 지양

2021년 9월 15일 • ☕️ 5 min read

clean code

변수를 재할당하는 것은 코드의 히스토리를 바꾸는 것과 같다. 다음을 보자.

let pizza = { fillings: ['bacon', 'mozzarella'] };

다음과 같은 이유로 피자에 베이컨과 모짜렐라 치즈가 들어 있는지 확신이 불가능하다.

  • 변수는 타입이 달라도 재할당 가능하다.
  • 값이 배열 또는 객체인 경우 내용 변경이 가능하다.

위 예시를 볼 때 우리는 현재 어떤 리스크가 있는지 생각해야한다. 하지만 이를 인지하고 있는 것은 불필요하고 부담일 수 있다.

그리고 대부분의 경우 둘 다 피할 수 있고, 이번 챕터에서는 그중에 첫번째인 변수 재할당에 대해 써보고자 해보고자 한다.

파라미터 재사용

간혹 변수는 다른 값을 저장하기 위해 재사용된다.

function getProducts(category) {
  category = loadCategory(category);
  category = category.filter(product => product.onSale);
  return category;
}

여기에서 category 변수는 카테고리 ID, 카테고리의 제품 목록 및 필터링된 제품 목록을 저장하는 데 사용된다. 이 함수는 짧기 때문에 큰 문제가 있는 것은 아니지만 재할당 사이에 많은 코드량이 있다 생각해보자. 또한 새로운 값은 함수 argument shadowing이라고 하는 함수 인수에 새 값이 재할당 된다.

이 경우는 수정하기 아주 쉽다. 각 값에 대해 별도의 변수를 사용하면 된다.

function getProducts(categoryId) {
  const products = loadCategory(categoryId);
  return products.filter(product => product.onSale);
}

이를 통해 각 변수의 수명을 단축하고 명확한 변수명을 지을 수 있다. 또한 코드를 더 쉽게 이해할 수 있으며 각 변수의 현재 값을 파악하기 위해 파악해야 할 코드 량이 줄었다.

증분 계산

재할당을 위한 일반적인 사용 사례는 증분 연산일 것이다. 다음 예를 살펴보자.

  const validateImage = (image) => {
    let errors = '';

    if (!validateImageFileAndUrl(image.imageFiles)) {
      errors = errors + ERROR_MESSAGES.InvalidImageFiles;
    } // 파일 또는 URL 중 하나가 있어야 한다.

    if (!validateImageURL(image.imageFiles)) {
      errors = errors + ERROR_MESSAGES.InvalidImageURL;
    } // 이미지는 URL은 올바른 링크여야 한다.

    if (!image[INPUT_TYPES.Title]) {
      errors = errors + ERROR_MESSAGES.BlankTitle;
    } // Title은 비워 둘 수 없다.

    if (!image[INPUT_TYPES.Id].match(ID_PATTERN) !== null) {
      errors = errors + ERROR_MESSAGES.InvalidId;
    } // ID는 영문과 숫자로 이루어져야 한다.

    return errors;
  };

위 코드는 실패한 유효성(validation) 검사에 대한 에러 메세지를 문자열 변수에 추가한다. 그러나 지금은 에러 메세지 포매팅 코드가 유효성 검사 코드로 복잡하게 되어 보기 어렵다. 이로 인해 읽기 어렵고 유지보수하기 어렵게 만든다. 다른 유효성 검사를 추가하려면 포매팅 코드를 이해하고 복사해야하며, 또는 HTML으로 오류를 렌더링하려면 이 함수의 각 행을 변경해야 한다.

유효성 검사와 포매팅 코드를 분리해 보자.

const IMAGE_VALIDATIONS = [
  {
    // 파일 또는 URL 중 하나가 있어야 한다.
    isValid: (image) => validateImageFileAndUrl(image.imageFiles),
    message: ERROR_MESSAGES.InvalidImageFiles,
  },
  {
    // 이미지는 URL은 올바른 링크여야 한다.
    isValid: (image) => validateImageURL(image.imageFiles),
    message: ERROR_MESSAGES.InvalidImageURL,
  },
  {
    // Title은 비워 둘 수 없다.
    isValid: (image) => Boolean(image[INPUT_TYPES.Title]),
    message: ERROR_MESSAGES.BlankTitle,
  },
  {
    // ID는 영문과 숫자로 이루어져야 한다.
    isValid: (image) => image[INPUT_TYPES.Id].match(ID_PATTERN) !== null,
    message: ERROR_MESSAGES.InvalidId,
  },
];

const validateImage = (image) => {
  return IMAGE_VALIDATIONS
    .map(({ isValid, message }) => (isValid(image) ? undefined : message))
    .filter(Boolean);
};

const printimageErrors = (image) => {
  console.log(validateImage(image).join('\n'));
};

유효성 검사 및 포매팅 코드를 분리했다. 코드의 각 부분은 단일책임원칙을 가진다. 이제 유효성 검사는 조건 및 문자열과 섞이지 않고 선언적으로 정의 되어 tables처럼 읽힌다. 이 모든 것이 코드의 가독성과 유지보수성을 향상 시키고 있다. 또한 새로운 유효성 검사를 추가하기 쉽고, 유효성 검사 또는 포매팅 코드를 더 이상 세세하게 알고 있을 필요가 없다.

에러 메세지를 HTML 목록으로 렌더링할 수도 있다.

function ImageUploader() {
  const [image, setImage] = useState();
  const errors = validateImage(image);
  return (
    <>
      <FileUpload value={image} onChange={setImage} />
      {errors.length > 0 && (
        <>
          <Text variation="error">😱 업로드 실패 :</Text>
          <ul>
            {errors.map(error => (
              <Text key={error} as="li" variation="error">
                {error}
              </Text>
            ))}
          </ul>
        </>
      )}
    </>
  );
}

각 유효성 검사를 각각 테스트 가능하게 되었다. 또 ERROR_MESSAGES를 다른 곳에서 재사용하지 않는 한 상수를 인라인으로 작성 해도 된다. 하지만 가독성을 높이는 코드는 아니다. 그리고 두 곳 이상 변경해야 하는 경우엔 변경하기 더 어려워질 수 있다.

const IMAGE_VALIDATIONS = [
  {
    // 파일 또는 URL 중 하나가 있어야 한다.
    isValid: (image) => validateImageFileAndUrl(image.imageFiles),
    message: "파일 또는 URL이 입력 되어야 합니다.",
  },
  ...,
  ...,
]

상단 변수 선언

종종 함수의 시작 부분에 모든 변수를 정의 하는 스타일이 보인다. 실제로 시작 부분에 모든 변수를 선언 해야만 하는 언어도 존재하지만 사람들은 필요하지 않은 언어에서 이 스타일을 사용한다.

let isFreeDelivery;

// 50 lines of code

if (DELIVERY_METHODS.includes(deliveryMethod)) {
  isFreeDelivery = 1;
} else {
  isFreeDelivery = 0;
}

// 30 lines of code

submitOrder({ products, address, isFreeDelivery });

변수의 수명이 길어지면 변수의 현재 값을 예측하기 어려워진다. 재할당이 가능한 경우라면 상황은 한층 악화된다. 변수선언부와 사용부 사이에 50줄의 코드가 있으면 50줄 중 어디서든 재할당 될 수 있다.

변수선언부를 가능한 사용부에 가깝게 선언하고 재할당을 피함으로써 코드를 읽기 쉽게 만들 수 있다.

...,
...,
const isFreeDelivery = DELIVERY_METHODS.includes(deliveryMethod);
submitOrder({
  products,
  address,
  isFreeDelivery: Number(isFreeDelivery)
});

우리는 isFreeDelivery의 수명을 단축 시켰다.

결론

변수를 재할당 한다고 해서 나쁜것만은 아니고 재할당을 모두 제거한다해도 코드가 개선되는 것도 아니다. 재할당은 질문에 가깝다. 재할당 되는 코드가 보인다면 재할당 없이 코드를 다시 쓰는 것이 가독성 좋은 코드가 될지 생각해보자. 개인의 생각에 따라 정답이 달라지겠지만 재할당을 사용 해야 하는 경우엔 변수의 현재 값을 명확히 유추할 수 있는 작은 함수로 구분하자.

위의 모든 예제는 letconst로 대체하고 있다. 이렇게 하면 변수가 재할당되지 않음을 독자에게 알려주고 있다. 코드를 재할당 하는 것을 볼 때마다 이 코드가 더 복잡할 수 있고 이해하기 위해 더 많은 생각을 필요로 하다는 것을 이 글을 읽었다면 알 것이다.

여기서 또다른 유용한 규칙은 UPPER_CASE 상수 이름을 사용하는 것이다. 이것은 개발자에게 계산 된 결과가 아닌 구성 값에 더 가깝다는 것을 알려준다. 이러한 상수의 수명은 보통 전체 모듈 또는 전체 코드베이스와 함께 하기 때문에 코드를 읽을 때 상수가 정의된 부분은 볼 수 없지만 값은 변경 되지 않음을 인지할 수 있따. 그리고 함수에서 이러한 상수를 사용하더라도 상수는 99% 변하지 않을 것이기 때문에 거의(?) 순수함수다.

상수는 인지 부하를 줄이고 코드를 쉽게 이해 할 수 있도록 한다. 하지만 Javascript 에서 상수는 진짜가 아니다. 즉 변경이 가능 하다는 말이다. 다음 챕터 에서 이야기 해보자