[FIFAPulse] 개발기록 - 하위 컴포넌트 렌더링 오류 해결하기 (Non-null-assertion을 남발하면 안되는 이유)

조민호·2023년 5월 2일
0

문제 상황


  • 내 기록 페이지를 들어가게 되면 매치 타입별 최신 순으로 내 경기 기록을 보여준다

  • 그 경기를 클릭하게 되면 해당 매치의 세부 통계를 보여주는 방식이다

  • 세부 통계를 보여주는 MatchResult.tsx 에서 수치와 그래프를 보여주는 부분은

    하위 컴포넌트인 MatchStatistics.tsx로 에서 보여주고 있다

  • 그러므로 MatchResult.tsx 에서 MatchStatistics.tsx로 props를 통해 매치 데이터들을 넘겨주면

    MatchStatistics.tsx에서 본격적으로 수치와 그래프로 통계를 보여주는 것이다


근데 이 과정에서 TypeScript를 적용하다 보니 데이터가 제대로 넘어가지 않았던 문제가 발생했다


MatchResult.tsx는 대략적으로 아래와 같다

import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import MatchStatistics from '../../Components/MatchStatistics';
import { useUserObjAPI } from '../../Context/UserObj/UserObjContext';
import FIFAData from '../../Services/FifaData';
import { MatchDetail } from '../../types/api';
import { myDataIndex, selectedUsertStatistics } from '../../types/states';

const MatchResult = () => {
	const { matchId } = useParams();
  const [matchDetail, setMatchDetail] = useState<MatchDetail | null>(null); // 한 매치의 전체 데이터
  const [myDataIndex, setMyDataIndex] = useState<myDataIndex | null>(null); // 로그인 한 유저의 데이터 인덱스 (1대1 이므로 matchDetail.matchInfo[0] 아니면 matchDetail.matchInfo[1])
  const { userObj, setUserObj } = useUserObjAPI()!;
  const [selectedUsertStatistics, setSelectedUsertStatistics] = useState<selectedUsertStatistics>(0); 
	// 유저의 이름을 클릭함에 따라 보여줄 데이터의 인덱스 (1대1 이므로 matchDetail.matchInfo[0] 아니면 matchDetail.matchInfo[1])

	useEffect(() => {
    // 최초에 api를 불러와서 데이터 세팅
    const getMatchDetail = async () => {
      const fifa = new FIFAData();
      const result = await fifa.getMatchDetail(matchId!);
      let indexInfo = {} as myDataIndex; // 확실한 상황이므로 type assertion 사용

      // matchDetail.matchInfo[0]이 현재 로그인한 계정이라면 
      result.matchInfo[0].accessId === userObj?.FIFAOnlineAccessId ? (indexInfo = { mine: 0, other: 1 }) : (indexInfo = { mine: 1, other: 0 });

      setMyDataIndex(indexInfo);
      setMatchDetail(result);
      setSelectedUsertStatistics(indexInfo.mine); // 디폴트로 자기 자신의 정보를 보여줌
    };
    getMatchDetail();
  }, []);

	  const onUserNicknameClick = (choose: string) => {
    let index: 0 | 1 = 0;
    if (choose === 'mine') {
      index = myDataIndex!.mine;
    }
    if (choose === 'other') {
      index = myDataIndex!.other;
    }
    setSelectedUsertStatistics(index);
  };

  return (
		<div>
      <div>
        <button type="button" onClick={() => onUserNicknameClick('mine')}>
          {matchDetail?.matchInfo[myDataIndex!.mine].nickname}
        </button>
        <h1>vs</h1>
        <button type="button" onClick={() => onUserNicknameClick('other')}>
          {matchDetail?.matchInfo[myDataIndex!.other].nickname}
        </button>

			...
  		{/* 전체 스코어 결과를 보여주는 부분 (생략) */}
      </div>
	    
		  {/* MatchStatistics 에서 본격적인 통계를 보여줌 */}
      {matchDetail && myDataIndex && (
        <MatchStatistics matchDetail={matchDetail} myDataIndex={myDataIndex} selectedUsertStatistics={selectedUsertStatistics} />
      )}
      {/* <MatchStatistics matchDetail={matchDetail!} myDataIndex={myDataIndex!} selectedUsertStatistics={selectedUsertStatistics!} /> */}
    </div>
  );
};

export default MatchResult;


여기서 MatchStatistics를 하위 컴포넌트로 사용할 때 ,

props를 최초에는 아래의 방식으로 넘겨줬다

<MatchStatistics matchDetail={matchDetail!} myDataIndex={myDataIndex!} selectedUsertStatistics={selectedUsertStatistics!} />

matchDetail과 myDataIndex은 api를 통해서 정해지는 것이므로 초깃값을

아예 null 타입으로 지정해 줬으므로 props로 넘겨줄때도 역시 non-null-assertion을 사용해야만 했다



근데 이렇게 넘기고 MatchStatistics.tsx를 들어가게 되면

  • 최초에는 아무 문제 없이 데이터가 제대로 들어오지만

  • MatchStatistics.tsx 페이지에서 새로고침을 하면 props들이 null이 돼버린다


이유는 간단하다

새로고침을 하면 MatchResult.tsx가 리렌더링 되는데 이 때

  1. JSX를 먼저 그리고
  2. useEffect()가 진행되므로

1번의 과정에서 이미 MatchStatistics도 호출이 되므로 이 때 props로 null값들이

먼저 넘어가 버리는 것이다


이래서 왠만하면 non-null-assertion같은 타입을 단지 에러를 피하기 위한 용도로 남발하면 안되는 것이다



해결


matchDetail, myDataIndex, selectedUsertStatistics 데이터들을 굳이 props로 넘겨주지 않고 다른 방법으로 사용해서 해결이 가능하다

그렇지만 이런 데이터들은 매치 하나당 하나씩 존재하기에 종류가 너무 많고

수도 없이 갱신이 된다

또한 클릭할 때마다 유저별로 데이터를 바꿔서 보여줘야 하기에

이렇게 종류가 다양하고 가변적인 데이터들을 DB에 넣는건 오히려 비효율적인 것 같았고

그렇다고 로컬스토리지나 , 전역 상태로 관리하는 것도 굳이 하위 컴포넌트 한 곳에서만

사용할 데이터를 그렇게 거창하게 사용할 필요는 없어 보였다

그래서 기존 방식을 그대로 고수하면서 수정을 해 보았다


바로 , && 연산자를 사용해서 기존 데이터들이 다 준비가 되면 하위 컴포넌트를

불러서 props를 전달해주는 것이다

{matchDetail && myDataIndex && selectedUsertStatistics &&(
        <MatchStatistics matchDetail={matchDetail} myDataIndex={myDataIndex} selectedUsertStatistics={selectedUsertStatistics} />
      )}

이렇게 하면 데이터들이 전부 useEffect를 통해 세팅이 되었을 때 하위 컴포넌트를 불러오면서
props로 데이터를 넘겨줄 것이다


그렇지만 여기서 또 문제가 발생했다

selectedUsertStatistics 의 값은 0 아니면 1 만 들어가게 타입을 지정해 놓았다

그렇지만 알다시피 JS의 0과 1은 truthy , falsy 로 적용이 돼서

  • 내가 의도한 것은 0 이든 1이든 둘다 유효한 값이므로 그 값이 전달이 돼야 하는데

  • 0일 경우에는 falsy로 작동해 버려서 아예 하위 컴포넌트 렌더링 자체가 안 되는 현상이 발생하는 것이었다


그러므로 selectedUsertStatistics 부분을 조건부 로직에서 제거했다

그래도 되는게 어차피 matchDetail 와 myDataIndex 는 초깃값이 null이므로

반드시 조건을 걸어줘야 하지만

selectedUsertStatistics 는 어차피 null 자체가 나올 수가 없기 때문이다

( 초깃값이 0 이고 useEffect가 진행되면 수정이 되는데 어차피 0 아니면 1 이므로 )

{matchDetail && myDataIndex && (
        <MatchStatistics matchDetail={matchDetail} myDataIndex={myDataIndex} selectedUsertStatistics={selectedUsertStatistics} />
)}
profile
할 수 있다

0개의 댓글