[FIFAPulse] 개발기록 - ‘…’ 은(는) 'null'일 수 있습니다 해결하기

조민호·2023년 5월 3일
0

문제 상황


리액트+TypeScript 로 프로젝트를 진행하던 도중 ,

정말 수도 없이 만나본 에러중 하나가 바로 이 에러이다

해당 에러가 발생하는 이유는 데이터 자체가 비어 있을 수도 있는 상황에서

데이터에 접근하려고 할 때 TypeScript 컴파일러가 혹시 모를 상황을 대비해 에러를

발생시키는 것이다


우선 예시로 들어볼 코드는 아래와 같다

interface MatchDetail {
  matchDate: string;
  matchId: string;
  matchType: number;
}

...
	
const [matchDetail, setMatchDetail] = useState<MatchDetail | null>(null);
  • matchDetail이라는 상태값이 존재하고

  • 초기값은 null로 줬으며 , 타입은 null 혹은 MatchDetail 가 될 수 있다


이런 상황에서 JS였다면 그냥 변수 형태 그대로 사용했었겠지만

현재 matchDetail 상태의 타입은 MatchDetail 과 null 둘 다 들어올 수 있으므로

TS에서는 에러를 발생하게 된다

console.log(matchDetail.matchId)
// 'matchDetail'은(는) 'null'일 수 있습니다.


해결


1. if 문 사용하기

가장 쉬운 방법이다

if문을 통해 해당 값의 존재 유무에 따른 조건부 로직을 작성하면 된다

그렇지만 , 리액트의 JSX에는 if문을 사용할 수 없으므로 크게 와닿는 방법은 아니다


2. && 로 걸러내기

matchDetail && matchDetail.matchId

&& 특성에 의해 , matchDetail이 만약 존재한다면(=null이 아니라면)

matchDetail.matchId에 접근하는 것이다


3. 옵셔널체이닝 사용 (BEST)

세번째 방법은 옵셔널체이닝을 사용하는 방법이다

그렇다면 옵셔널 체이닝에 대해서 우선 알아보자


옵셔널 체이닝이란?

옵셔널체이닝 연산자 (?.) 는 객체 내의 key에 접근할 때
그 참조가 유효한지 아닌지 직접 명시하지 않고도 접근할 수 있는 연산자이다

?. 연산자 앞의 평가대상이 만약 nullish ( undefined 또는 null ) 일 경우 평가를 멈추고
undefined를 반환하게 된다


아래 코드를 보면 john은 score와 english 라는 key가 존재하지 않으므로 에러가 발생한다
const students = {
    mark: {
        age: 20,
	    	score: {
            korean: 90,
            english: 80,
            math: 40
        }
    },
    john: {
        age: 20,
    }
}

console.log(students.mark.score.english); // 80;
console.log(students.john.score.english); // TypeError: Cannot read properties of undefined (reading 'english')

물론 이런 경우에도 두번째 방법으로 언급했던 것처럼 && 연산자를 통해 해결이 가능하다

console.log(students.john.score && students.john.score.english);

&& 특성에 의해 , join에게 score라는 key가 있다면 .score.english에 접근하기 때문에

에러가 나지 않고 undefined만 반환하게 된다

그렇지만 이 방법은 검사할 항목이 많아 질수록 코드의 길이가 길어지게 된다

예를 들어 john이라는 key가 마저 존재하지 않아도 작동하게 해야 한다면

이렇게 코드가 늘어나게 된다

console.log(students.john && students.john.score && students.john.score.english);

이럴때 옵셔널 체이닝을 사용하게 된다면 코드가 굉장히 간결해진다

console.log(students.john?.score?.english);

?. 연산자의 왼쪽에 있는 것을 평가한뒤 undefined 또는 null이라면

undefined를 반환하고 평가가 끝난다

만약 undefined 또는 null이 아니라면 계속해서 평가가 이루어지게 되는 것이다


그러므로 mark의 english 성적을 반환하는 함수의 경우 이렇게 작성하면

간결하게 값을 반환할 수 있게 된다

function getFriendAge(user) {
	return students.mark?.score?.english
}

대괄호 표기법에도 옵셔널 체이닝이 가능하고

const user = {
	info: {
    	firstName: 'hello world'
    }
};
const key = "firstName";
const userName = user.info?.[key];

메서드에도 사용이 가능하다

const some = {
	customMethod: function() {
    	console.log('hello optional');
    }
}

let result = some.customMethod?.(); // hello optional

배열에도 사용이 가능하다

console.log(arr?.[42]); // undefined 

console.log(arr[42]); // TypeError: Cannot read properties of undefined (reading '42')


💡 그렇지만 옵셔널 체이닝은 원활한 디버깅을 위해 존재하지 않아도 괜찮은 대상에만 적용해야 한다. 단지 에러를 피하기 위해서 남용하면 안된다는 것이다

만약 사용자 객체는 꼭 있어야하는데 그 안에 info 의 age는 꼭 필수가 아니라면

user에는 옵셔널 체이닝을 사용하면 안 되는 것이다

console.log(user?.info?.age) // user 객체가 꼭 있어야할 필요가 없다는 뜻

console.log(user.info?.age); // user 객체가 없다면 에러 발생



다시 본론으로 돌아와서 , 기존 에러에 적용을 하면 이렇게 된다

console.log(matchDetail?.matchId)

matchDetail가 존재한다면(=null이 아니라면) matchDetail.matchId를 출력하고

존재하지 않는다면 그냥 무시하게 되는 것이다


3. 타입 단언 사용 (type assertion)

matchDetail 은 현재 MatchDetail객체 타입이거나 null 이거나 둘 중 하나이다

그렇다면 matchDetail를 사용할 때 , as 키워드로 type assertion을 사용해서

이건 절대 null이 아니라는 것을 보장하는 것이다

console.log((matchDetail as MatchDetail).matchId);

그렇지만 타입 단언은 실제로 정말 주의해서 사용해야 한다

TypeScript의 장점은 컴파일 단계에서 에러를 잡지 않고 그냥 넘겨버리기 때문에

실제 런타임 상황에서 에러가 발생 할 수 있기 때문이다

interface Hero {
  name: string;
  skill: string;
}

const a = {} as Hero; // name, skill이 없어도 에러가 발생하지 않음 
a.name = "ss"; // 타입 추적 정상 작동

4. non-null assertion 사용하기

non-null assertion 은 말 그대로 null이 아니라는 것을 보장한다는 것이다

이 또한 type assertion과 굉장히 비슷하다

  • type assertion은 해당 타입이라고 완벽히 보장하면서 에러를 넘기는 것이고
  • non-null assertion은 null이 아니라고 완벽히 보장하면서 에러를 넘기는 것이다
console.log(matchDetail!.matchId);

또 다른 문제

그렇다면 아래의 상황에서는 어떻게 해야 할까?

interface DataFlag {
  mine: 0 | 1;
  other: 0 | 1;
}

interface MatchDetail {
  matchDate: string;
  matchId: string;
  matchType: number;
  matchInfo: [matchInfoType, matchInfoType];
				// 인덱스로 DataFlag 객체의 mine,other 값을 사용
}

...
	
const [matchDetail, setMatchDetail] = useState<MatchDetail | null>(null);
const [dataFlag, setDataFlag] = useState<DataFlag | null>(null);
  • 2개의 상태값이 각각 존재하고

  • 초기값은 null로 줬으며

  • matchDetail.matchInfo[dataFlag.mine] 형태로 사용되는 구조이다

    ( matchDetail에 존재하는 matchInfo라는 속성이 배열이고 ,

    이 배열의 인덱스로 dataFlag를 사용한다는 소리 )


이런 상황에서 아래와 같이 접근하게 된다면

matchDetail과 dataFlag 둘 다 null 일 수 있다는 에러가 발생하게 된다

matchDetail.matchInfo[dataFlag.mine]

이런 경우에는 위에서 언급한 것처럼 옵셔널 체이닝으로 해결하려고 했지만

이상하게 dataFlag에서 에러가 발생한다

console.log(matchDetail?.matchInfo[dataFlag?.mine]);
													// 'undefined' 형식을 인덱스 형식으로 사용할 수 없습니다.

말했듯이 , 옵셔널 체이닝은 만약 ?. 앞의 값이 존재하지 않는다면

undefined가 반환된다고 했다

그러므로 이런 에러가 발생하는 것이다



해결

이런 경우에는 어쩔 수 없이 undefined를 반환하는 옵셔널 체이닝보다,

  • 직접 그 값을 반환할 수 있도록 캐스팅을 해주거나

  • && 를 통해 undefined를 반환하는게 아니라 애초에 실행 자체를 못 하도록

    조건을 걸어줘야 한다

	console.log(matchDetail?.matchInfo[dataFlag!.mine]); // dataFlag는 무조건 값이 존재함
  console.log(matchDetail?.matchInfo[(dataFlag as DataFlag).mine]); // dataFlag는 null이 아닌 DataFlag타입이 됨
	// non-null-assertion 과 type assertion을 통해 캐스팅 

  console.log(matchDetail && dataFlag && matchDetail.matchInfo[dataFlag.mine]);
 // matchDetail 과 dataFlag 가 존재할 경우에만 접근가능하도록 함
profile
할 수 있다

0개의 댓글