React 프로젝트에서 삼항 연산자 줄여보기

React 프로젝트에서 삼항 연산자 줄여보기

React 프로젝트에서 가독성을 향상시키기 위해 삼항 연산자를 줄이는 방법을 찾아봤습니다.

·

3 min read

삼항 연산자

리액트 프로젝트에서 조건부 렌더링할 때 삼항 연산자를 사용할 수 있다.

  • fetchApi는 특정 데이터를 요청하는 임의의 함수이다.

  • <Person />은 특정 데이터를 렌더링하는 컴포넌트이다.

function App() {
  const queryKey = {
    key: "fetchPeople",
  };
  const {
    data: people,
    isLoading,
    error,
  } = useSWR(queryKey, () => {
    return fetchApi();
  });

  return (
    <div>
      <h1>People</h1>
      {!people ? (
        <p>Empty data</p>
      ) : (
        people.map((person) => {
          return <Person key={person.id} person={person} />;
        })
      )}
    </div>
  );
}

삼항 연산자를 JSX 문법 내부에 사용하기 위해선 중괄호, 물음표, 콜론 등을 작성해야 한다. 삼항 연산자가 여러 개 중첩되면 컴포넌트의 가독성이 떨어진다는 이슈가 있었다. 따라서 삼항 연산자를 대체할 수 있는 별도의 컴포넌트를 작성해 보기로 했다.

두 가지의 필요조건이 있었다.

  • 데이터는 객체 형태로 전달되고, 객체 내 모든 요소가 유효한 값일 때만 보이게 하고 싶다. 즉 객체에 null 또는 undefined 프로퍼티가 없어야 한다.

  • 데이터가 없어도 특정 인자가 true일 때 컴포넌트가 보여야 한다.

  • Stricted 타입은 객체의 모든 요소가 null 또는 undefined가 아님을 나타내기 위해 작성한 유틸리티 타입이다.

Guard 컴포넌트

Props에 children 항목을 함수 형태로 넘겨줄 수 있었고 컴포넌트에 children 항목을 넘겨줄 때는 속성을 직접적으로 명시하지 않고 children 위치에 컴포넌트를 어떻게 렌더링할지를 함수로 작성해주면 된다.

interface LayoutProps {
  children: () => ReactNode;
}
const Layout = (props: LayoutProps) => {
  return props.children();
};
const Section = () => {
  return (
    <Layout>
      {() => {
        return <p>Section</p>;
      }}
    </Layout>
  );
};

GuardProps 타입은 객체 형태의 데이터가 주어질 때 그리고 주어지지 않을 때 두 가지로 나뉘어진다.

isOk로 내부 컴포넌트를 렌더링할지 판단할 수 있고, 결정되는 과정은 다음과 같을 것이다.

  • data가 주어지면 data에 undefined 또는 null 값의 속성이 있는지 검사하고 하나라도 있으면 isOk 변수는 false를 나타낼 것이다.

  • data가 없으면 when 인자로 판단한다.

isOK가 참일 때는 Props로 전달된 내부 컴포넌트가 렌더링된다. 거짓일 때는 따로 fallback으로 지정한 컴포넌트가 없으면 아무것도 렌더링되지 않는다.

import { ReactNode, useMemo } from "react";

type Stricted<T extends Record<string, unknown>> = {
  [key in keyof T]-?: NonNullable<T[key]>;
};

export type GuardProps<T extends Record<string, unknown>> =
  | {
      data?: undefined;
      when: boolean;
      children: () => ReactNode;
      fallback?: ReactNode;
    }
  | {
      data: T;
      when?: true;
      children: (data: Stricted<T>) => ReactNode;
      fallback?: ReactNode;
    };

export const Guard = <T extends Record<string, unknown>>(
  props: GuardProps<T>
) => {
  const { fallback, children } = props;

  const isOk = useMemo(() => {
    if (props.data === undefined) {
      return props.when;
    } else {
      const values = Object.values(props.data);
      return values.every((value) => value !== undefined && value !== null);
    }
  }, [props.data, props.when]);

  const component = useMemo(() => {
    if (!isOk && !fallback) {
      return <></>;
    }
    if (!isOk && !!fallback) {
      return <>{fallback}</>;
    }
    if (!props.data) {
      return (children as () => ReactNode)();
    } else {
      return children(props.data as Stricted<T>);
    }
  }, [children, isOk, fallback, props.data]);

  return component;
};

사용 예시는 다음과 같았다.

사용자가 입력한 title이 10자 이상일 때와 아닐 때를 구분할 수 있다.

import { ChangeEvent, useEffect, useState } from "react";
import { Guard } from "~/components/Guard";

export default function Home() {
  const [title, setTitle] = useState("");
  const onChange = function (event: ChangeEvent<HTMLInputElement>) {
    setTitle(event.target.value);
  };

  return (
    <section>
      <Guard
        data={{ title: title.length < 10 ? null : title }}
        fallback={<span>10자 이상 입력</span>}
      >
        {({ title }) => {
          return <p>Input: {title}</p>;
        }}
      </Guard>
      <input type="text" value={title} onChange={onChange} />
    </section>
  );
}