[좋은 코드, 나쁜 코드] 6장. 예측 가능한 코드를 작성하라

cje·2023년 7월 10일
0
post-thumbnail

함수가 아무것도 반환하지 않을 때가 있거나 처리해야 할 특별한 시나리오가 있는 경우 이 사실을 다른 개발자에게 확실하게 알려야 한다.


매직값을 반환하지 말아야 한다.

적극적으로 경계하지 않으면 정상적인 반환값으로 오인하기 쉽다.

예를 들어 메서드 indexOf가 특정 값을 찾지 못하면 -1을 반환하는 경우를 말한다.

해결책: 널, 옵셔널 또는 오류를 반환하라.

함수에서 매직값을 반환할 때의 문제점은 호출하는 쪽에서 함수 계약의 세부 조항을 알아야 한다는 점이다.

그래서 널 안정성을 지원하는 경우에는, 널이 가능한 유형을 반환하고
널 안정성을 지원하지 않는 경우에는, 옵셔널 값을 반환하는 것이 낫다.

널값이나 비어 있는 옵셔널을 반환하는 것의 단점은 값이 없는 이유를 명시적으로 전달하지 않는다는 점이다.

이 부분은 fp-ts를 활용해 명시적으로 이유를 전달할 수 있을 것 같다.

하지만 널을 반환하는 경우의 함수는 호출하는 곳에서 널을 반환하는 경우를 처리해야 하는 번거로움이 생긴다. 그렇지만 어떤 값이 존재하지 않을 수 있고 이 사실을 분명히 하지 않는다면 더 많은 버그를 발생시킬 수 있고, 버그를 처리하고 고치는 비용이 널 반환값을 올바르게 처리하기 위해 코드를 추가하는 비용보다 훨씬 높을 수 있다.

때때로 매직값이 우연히 발생할 수 있다.

const getMinValue = (values) => {
	let minValue = Number.MAX_SAFE_INTEGER;
  
  	values.forEach((value) => {
    	minValue = Math.min(value, minValue);
    })
  	
  	return minValue;
}

위 코드의 문제점은 values 배열에 요소가 없는 경우, Number.MAX_SAFE_INTEGER 값을 가진 minValue를 그대로 반환한다는 것이다. 따라서 getMinValue 함수를 호출하기 전에 values 배열이 비어있는지 확인하거나 내부에서 확인할 필요가 있다.

가장 좋은 것은 언어의 기존 유틸리티가 있다면 그것을 활용하는 것이 좋다. Math.min(...values)


널 객체 패턴을 적절히 사용하라

값을 얻을 수 없을 때 널값이나 빈 옵셔널을 반환하는 대신 널 객체 패턴을 사용할 수 있다.

널 객체 패턴을 사용하는 이유는 널 값으로 인해 시스템이 중단되는 상황을 막기 위함이다. 하지만 오류가 발생했다는 사실을 숨기기 위해 널 객체 패턴을 사용하는 것은 바람직하지 않다.

함수를 호출할 때 널 객체 패턴을 사용하는 것은 본질적으로 빈 상자를 파는 것과 같다.

const getRandomMug = (inventory) => {
	if(!inventory.length) {
    	return { diameter: 0, height: 0 };
    }
  
  	return inventory[Math.floor(Math.random() * inventory.length)];
}

위 코드에서 재고가 없을 때 null을 반환하지 않고 크기가 0인 머그잔 객체를 생성해서 반환했다. 해당 함수를 호출할 때 널을 처리하지 않아도 되기에 코드 작성이 간단해지지만 부정확한 값을 반환해 예상을 벗어나는 결과를 초래할 수 있다. 차라리 이 경우에는 머그잔이 없는 경우 명시적으로 null을 반환하는 것이 좋다.


예상치 못한 부수 효과를 피하라

부수효과는 어떤 함수의 호출이 함수 외부에 초래한 상태 변화를 의미한다. 함수가 반환하는 값 이외에 다른 효과가 있다면 이는 부수 효과가 있는 것이다.

해결책: 부수 효과를 피하거나 그 사실을 분명하게 하라

함수의 이름을 통해 부수 효과에 대해 분명하게 알 수 있도록 하는 것이 좋다.

어떤 함수가 부수 효과를 일으킨다면, 그 함수를 호출하는 쪽에서 이 사실에 대해 명백하게 알 수 있도록 하는 책임이 함수의 작성자에게 있다. 애초에 부수 효롸를 일으키지 않는 것이 예측 가능한 코드를 위해 가장 좋은 방법이지만, 실제로 그렇게 하기 어려운 경우도 있다. 부수 효과를 피할 수 없을 때 적절하게 이름을 짓는 것은 그 사실을 명백하게 나타내는 매우 효과적인 방법이다.


입력 매개변수를 수정하는 것에 주의하라

함수 내부에서 매개변수를 수정하지 않도록 해야 하며, 변경해야 하는 경우에는 변경 전에 새 자료구조에 복사한 뒤 그 값을 수정해야 한다.

📌 불변의 위험성
항상 불변을 유지해야 하는 것이 좋은 줄 알았는데 불변의 위험성이라는 키워드를 듣게 되었다.
recoil의 경우, 값이 계속 생성되어 기존 값은 사용하지 않더라도 지워지지 않는 메모리 누수 이슈가 있어 jotai를 소개해주셨다. 특이한 점은 recoil은 개발자가 직접 key를 주입해야 하는데 jotai는 key를 알아서 주입해준다는 점에서도 신기하고 놀랐다. 익숙한 recoil을 사용하곤 했는데 이번엔 jotai를 사용해보아야겠다!!!

막간을 이용해 recoil에 비해 jotai가 갖는 장점
1. 업데이트가 활발하다.
2. 모듈 size가 약 1/11이다.
3. js가 아닌 100% ts 프로젝트이다.
4. 문자열 key가 불필요하다. -> jotai는 object-referential identities를 이용하고 있음
5. provider 없이 사용 가능하다. -> provider-less mode
(출처: 스터디 내 엄청난 실력자 분, Jotai 레시피)

✏️ Immer
이번 장은 변이의 위험성을 이야기하고 있는 것 같아 불변성과 관련된 Immer 라이브러리가 생각났다. 실제로 회사에서 많이 쓰이기도 하지만 의도치 않게 리렌더링이 발생하지 않는 문제가 있어 유의해서 사용해야 함을 알 수 있었다!


미래를 대비한 열거형 처리

열거형을 처리해야 하는 경우 나중에 열거형에 더 많은 값이 추가될 수 있다는 점을 기억하는 것이 중요하다. 이것을 무시하고 코드를 작성하면, 자기 자신 혹은 다른 개발자들의 예측을 벗어나는 좋지 않은 결과를 초래할 수 있다.

이번 섹션에는 Enum에 관련된 코드를 예시로 들었지만 Enum은 이러한 이슈로 잘 사용하지 않기 때문에 다음과 같이 코드를 재작성해보았다.

const OUTCOME = {
	'GO_BUST': 'GO_BUST',
  	'MAKE_A_PROFIT': 'MAKE_A_PROFIT'
} as const;

const isOutcomeSafe = (prediction: keyof typeof OUTCOME) => {
	if (prediction == OUTCOME.GO_BUST) {
    	return false;
    }
  
  	return true;
}

위 코드는 앞으로 나중에 열거형 값이 더 추가될 수도 있음을 간과했다. 그 결과 불안정한 결과값을 반환하는 함수가 되었다.

해결책: 모든 경우를 처리하는 스위치 문을 사용하라

위 코드처럼 암시적인 방식으로 처리하지 말고 명시적으로 작성해 처리되지 않은 새로운 열거값이 추가되는 경우 코드 컴파일이 실패하거나 테스트가 실패하도록 하는 것이 좋다.

일반적인 방법으로는 모든 경우를 다 처리하는 스위치 문을 사용하는 것이다.

기본 케이스의 경우, 임의적인 값을 반환하는 것보다 예외를 발생시켜 명시적으로 해당 열거값이 처리되지 않았음을 나타낼 수 있다.

const isOutcomeSafe = (prediction: keyof typeof OUTCOME) => {
	switch (prediction) {
      	case OUTCOME.GO_BUST:
        	return false;
        case OUTCOME.MAKE_A_PROFIT:
        	return true;
      	default:
        	return false;	
    }
}
const isOutcomeSafe = (prediction: keyof typeof OUTCOME) => {
	switch (prediction) {
      	case OUTCOME.GO_BUST:
        	return false;
        case OUTCOME.MAKE_A_PROFIT:
        	return true;
      	default:
        	throw new Error('에러');	
    }
}

이렇게 작성한 뒤 해당 컴포넌트 상위에 ErrorBoundary를 두어 함수에서 던진 에러를 잡는 방식으로 에러를 핸들링하면 된다.

profile
💭

0개의 댓글