본문 바로가기
개발/Gistory

지스토리 프로젝트 회고록 2 : 정의되지 않음과 없음에 관해

by 개발곰 2022. 2. 23.

지스토리는 리액트로 만들어졌습니다. 당연히 JavaScript를 많이 쓸 수 밖에 없습니다. TypeScript로 마이그레이션 하기는 했지만 TS도 결국 JS로 돌아가기 때문에, JS의 문제점도 어느정도 가지고 가야 합니다. 특히 지스토리처럼 나중에 TS로 마이그레이션 한 경우는 다 엎을 수 없으니 이런 문제가 더 많습니다. 오늘은 그 문제 중 가장 대표적인 undefined와 null에 대해 논하겠습니다.

undefined와 null이란?

우리는 산술적으로 없음을 표기할 때 0이라는 값을 사용합니다. 그렇지만 이 것말고도 또 다른 값이 존재할 수도 있습니다. 예를 들어, 변수를 선언하고 값을 정의하지 않았다면, 이 변수의 값은 무엇이 되어야 할까요? 만약 변수를 사용하고, 그 값이 유효하지 않아서 변수를 값이 없다는 상태를 표현하고 싶을 때는 어떻게 해야할까요? 자바스크립트에서 전자의 경우는 undefined(값이 대입되지 않음)고, 후자를 null(값이 없음)로 표현합니다. 문제는 이런 값이 없음을 표현하는 값은 여러 오류를 일으킬 수 있습니다.

undefined와 null은 왜 문제가 될까요?

The worst mistake of computer science 이 글을 읽어 보시면 null을 왜 사용하면 안 되는지 알 수 있습니다. 짧게 요약하면, 값이 없음이라는 값이 모든 종류의 값에 대응될 수 있기 때문에 어떻게 행동할지 예측하기 힘들며, 이를 체크하기 위한 함수들을 추가해야 한다는 것입니다. 저 글에서 제시하듯이, 값이 없음이 가능한 변수와 아닌 변수를 분리함으로서 이를 방지할 수도 있습니다.

자바스크립트의 경우는 더 심각한데, 특이한 종류의 값이 없음인 undefined, 즉 값이 대입되지 않음을 대입할 수 있게 해 놓았습니다. 덕분에 따로 처리해줘야 하는 케이스가 늘어났습니다.

타입스크립트의 경우 엄격한 null 검사를 사용하면 undefined와 null에 대해서 트랜스파일 과정에서 따로 타입 체크를 해주기 때문에 이 위험에서 조금 더 벗어나기 쉬워집니다.

loadingReducer

여러분이 이 문제를 직접적으로 겪는 경우는 주로 스토어의 초기 상태와 관련된 문제입니다. 스토어는 기본적으로 initialValue로 넣어준 값을 반환합니다. 만약 적절한 상태를 다 만들어 놓지 않아서 비어 있는 값을 요구하면, undefined를 반환합니다. 즉 적절한 initialValue를 설계하지 않으면 여러분은 undefined를 마주하게 됩니다.

loadingReducer 로딩 중에는 true를 반환하고 로딩이 끝났으면 false를 반환합니다. 하지만 구현의 편의를 위해, initialValue는 그냥 비어있는 Object 입니다. 즉 처음에 로딩 정보를 요청하면 undefined를 반환합니다. loadingArticle이 게시물을 가져오는 액션의 로딩 상태라 합시다. 로딩이 끝났을 때(loadingArticle이 false 일 때) 컴포넌트 <Article / >을 로드시키려면 어떻게 해야 할까요? 이렇게 하면 될까요?

{
  !loadingArticle && <Article />;
}

안 됩니다. 아예 액션 자체가 실행되기 전에, 그러니 로딩의 진행상황이라는 개념이 없을 때는 컴포넌트가 보여져서는 안됩니다. 그러나 undefined는 falsy하기 때문에, 대부분의 상황에서 false와 비슷하게 작동합니다. 저렇게 하면 초기에 잠깐 빈 Article 컴포넌트가 보이고 로딩하는 동안 안 보이고, 그리고 로딩이 끝나면 다시 보이게 됩니다. 이걸 제대로 체크하려면 다음과 같이 바꿔야 합니다.

{
  loadingArticle === false && <Article />;
}

이런 falsy한 값의 타입체크를 typeof를 이용해 할 수 있을 것 같지만,typeof에는 매우 역사적이고 오래된 버그가 있습니다. 궁금하시면 typeof null을 콘솔 창에 넣어보세요. 설명. JS에는 하위 호환성 때문에 잘못된 설계를 방치하는 경우가 많은데, 이것도 그 예시입니다. undefined, null, boolean 구별에는 ===을 쓰셔야 합니다.

아무튼 다시 원래 주제로 돌아와서 저기에 로딩 중 일때, 즉 loadingArticletrue일 때 로딩 애니메이션이 보이게 하려면, 또 조건 문을 넣어줘야 합니다. falsy(undefined) 이거나 true 일때는 로딩 창이 보여야 하는데, 정작 false 일 때는 로딩 창이 아니라 원래 컴포넌트가 보여야 합니다. 이런 문제 때문에 === false, === undefined 같은 코드를 넣어 줘야 합니다.

이러한 일들이 반복적으로 일어나기 때문에 저희는 loadComponent를 만들어 놓았지만, 여기서 문제가 끝이 아닙니다. 만약 두 가지 액션이 끝난 다음에 컴포넌트를 보여줘야 하는 경우는 어떻게 해야 할까요? loadingArticleloadingCommentList가 둘 중 하나만 undefined 일 때도 둘 다 undefined 일 때와 똑같이 작동해야 합니다.

console.log(undefined && undefined); // undefined
console.log(undefined && false); // false

그렇지만 자바스크립트는, 이렇게 작동합니다. 단순히 AND로 묶어서는 해결되지 않습니다. 결국 저희는 다음과 같이 묶어서 해결해 주었습니다.

const loadingMain = (function getLoading() {
  if (loadingArticle && loadingCommentList) return true;
  else if (loadingArticle === false && loadingCommentList === false) return false;
  else return undefined;
})();

여기서 한 번 더 나아가, 일반적은 여러 개의 loading이 다 끝날 때까지 기다리려면 어떻게 해야할까요? 저 함수를 분석해 봅시다.

  1. 입력 받은 로딩 값들이 전부 다 true인지 검증해서 이를 AND 연산합니다.
  2. 입력 받은 로딩 값들이 전부 다 false 인지 검증해서 이를 AND 연산합니다.
  3. 1,2 둘 다 해당하지 않을 경우, 초기 상태인 undefined를 반환합니다.

즉 1,2에서 검증 로직만 분리하고, 결합 로직을 여러 인자에 대해서도 가능하게 만들면, 여러 loading에 대해서도 처리할 수 있게 됩니다.

export const combineLoading = function (...arr: Array<Loading>): Loading {
  if (arr.reduce((acc, current) => acc && current)) {
    return true;
  } else if (
    arr.map((value) => value === false).reduce((acc, current) => acc && current)
  ) {
    return false;
  } else {
    return undefined;
  }
};

여러분에게 추천드리고 싶은 것은, 소스코드가 좀 길어지더라고 falsy한 값들(null, [], undefined 등)에 대해 제대로 핸들링을 하는 것입니다. 이러한 값들은 제어문을 여러분이 의도한 것과 전혀 다르게 작동되게 할 수 있습니다. 그리고 TS의 타입체크를 적극적으로 활용하도록 타입을 명확하게 설계를 하시길 바랍니다.