5 minute read

📝 고민을 하게 된 계기

아래 코드는 프로젝트를 진행하던 중 공지사항 작성 페이지, 작성된 공지 삭제시 뜨는 모달과 관련한 코드이다. 컴포넌트를 정리하다 평상시 무의식적으로 작성했던 부분들에 대해 생각해보니, 그 이유에 대해서는 스스로 제대로 대답하지 못했다. 때문에 이번 기회에 차근차근 헷갈리는 부분을 포함하여 정리해보기로 했다.

const DeleteNoticeModal = ({
  noticeId,
  isOpen,
  closeModal,
  children,
}: DeleteNoticeModalProps) => {
  const { token } = useAuthStore();
  const navigate = useNavigate();

  if (!isOpen) return null;

  const handleDeleteClick = async () => {
    try {
      await Axios.delete(`/announcements/${noticeId}`, {
        headers: { Authorization: `Bearer ${token}` },
      });
      navigate('/view/all-notices');
    } catch (error) {
      console.error('삭제실패', error);
    }
  };

  return (
    <Wrapper $isOpen={isOpen}>
      <p>{children}</p>
      <button type="button" onClick={closeModal}>
        X
      </button>
      <DeleteButton **onClick={() => handleDeleteClick()}**>삭제하기</DeleteButton>
    </Wrapper>
  );
};
const ViewAllNotice = () => {
  const navigate = useNavigate();
  const { role } = useAuthStore();
  const { curPage, isSorted, setCurPage, setIsSorted } = usePageStore();
  const sortOptions: SortOptions = { desc: '최신순', asc: '오래된순' };
  const { notices, totalPage, isLoading } = useFetchNotices({
    isSorted,
    curPage,
  });

  const handleSort = (e: ChangeEvent<HTMLSelectElement>) => {
    setIsSorted(e.target.value as SortKey);
  };

  const handlePage = (index: number) => {
    setCurPage(index === 1 ? curPage + 1 : curPage - 1);
  };

  return (
    <Wrapper>
      <Header />
      <select **onChange={handleSort}**>
        {Object.entries(sortOptions).map(([key, value]) => (
          <option value={key} key={key}>
            {value}
          </option>
        ))}
      </select>
      {notices.map((notice) => (
        <NoticeCard
          key={notice.id}
          title={notice.title}
          content={notice.content}
          **onClick={() => navigate(`/view/detail-notice/${notice.id}`)}**
        />
      ))}

      <Layout>
        <button
          type="button"
          disabled={curPage === 0 || isLoading}
          onClick={() => handlePage(-1)}
        >
          {'<'}
        </button>
        <TempP>{`${curPage + 1}/${totalPage}`}</TempP>
        <button
          type="button"
          disabled={curPage + 1 === totalPage || isLoading}
          onClick={() => handlePage(1)}
        >
          {'>'}
        </button>
        {role === 'STUDENT_COUNCIL' && (
          <CreateBtn onClick={() => navigate('/create/notice')}>
            공지사항 작성하기
          </CreateBtn>
        )}
      </Layout>
    </Wrapper>
  );
};

DeleteNoticeModal 컴포넌트에서 <DeleteButton onClick={() ⇒ handleDeleteClick()}/> 과 ViewAllNotice 컴포넌트에서 <NoticeCard onClick={() ⇒ navigate(`/view/detail-notice/${notice.id}`)} /> 부분을 보면 DeleteButton은 handleDeleteClick이라는 함수명이 있지만 NoticeCard는 함수명이 존재하지 않는다. 이 둘의 차이는 뭐가 있는지 고민하면서 익명함수와 기명함수의 차이점, 추가적으로 setter 함수를 넘기는 과정에서 헷갈렸던 것이 있어 함께 곁들여 학습했다

기명함수와 익명함수

💡 useState의 setter함수와 콜백함수를 넘길 때 형태가 헷갈리는 부분을 정리해보자!

<예시>
const [count, setCount] = useState();
const handleSetCount = () ⇒ { };
1. `<span onClick={() => { setCount((a) => a + 1); }} >` (O) 2. `<span onClick={ setCount((a) => a + 1); } >` (X) 3. `<span onClick={handleSetCount} >` (O)

💡 1과 2의 차이점 및 오류

  • 1번, 2번중 옳은 사용방법이 1인 이유

2번과 같이 작성하는 경우에는 함수 실행 결과에 대한 값 자체가 들어간다. 즉, setCount((a) => a + 1);를 실행한 결괏값이 onClick에 전달되서 undefined가 들어가버리는 문제가 발생!

동시에 컴포넌트가 마운트되자마자 작동하므로 내가 원하는 타이밍인 클릭시에만 이벤트가 발생하지 않는다! 때문에 함수 자체를 전달해줘야하는 이유가 된다!

🧐 그러면 함수 자체를 전달하는 방식은 어떻게 될까?

함수 형태로 전달을 할 수 있는 방법은 우리가 흔히 아는 익명 함수(onClick={() ⇒ 함수명)})와 기명함수(onClick={함수명}) 형태가 있다.

둘 중에 더 좋은 방법은 후자인 기명함수이다. 코드가 더 깔끔하다는 동시에 최적화와 관련이 있다!

익명함수와 기명함수의 쓰임의 예시를 확인해보자!

예를 들어, 이벤트 핸들러에서 ‘e’를 사용하는 경우가 있다고 하자. 우리가 흔히 사용하는 방식은 아래와 같다.

const [keyword, setKeyword] = useState("");

<SearchInput
  onChange={(e: ChangeEvent<HTMLInputElement>) => {
    setKeyword(e.target.value);
  }}
/>;

그렇다면, 이를 기명함수 형태로 사용하면 어떻게 될까?

const [keyword, setKeyword] = useState("");

const handleKeyword = (e: ChangeEvent<HTMLInputElement>) => {
  setKeyword(e.target.value);
};

// e가 안들어가도 되는건 이벤트 해들러 자체에서 e를 감지하고 있기 때문에
// onChange={handleKeyword} 꼴이 가능하다
<SearchInput onChange={handleKeyword} />;

두가지 코드의 동작은 같지만 기명함수 형태가 훨씬 깔끔한 것을 한눈에 확인할 수 있다

그러면 e가 아닌 prev 값이 들어가는 경우에는 어떤 식으로 사용이 될까?

<span onClick={() => { setCount((a) => a + 1)}}></span> 는 클릭하는 경우에 값이 1씩 증가하는 코드이다. 마찬가지로 기명함수로 바꾸면 setCount를 사용하는 로직을 별도의 기명함수로 정의해주면 된다

const incrementCount = () => {
  setCount((a) => a + 1);
};

<span onClick={incrementCount}></span>;

기명함수로 분리하는 것이 코드가 길어 보일 수 있는 경우도 있으나 주로 가독성과 유지보수성을 위한 선택이기 때문에 기명함수 형태로 사용하는 것이 좋다고 한다

직접 테스트를 해보자!

import { useState, useEffect } from "react";

const Example = () => {
  const [anonCount, setAnonCount] = useState(0);
  const [count, setCount] = useState(0);

  const handleCount = () => {
    setCount((prev) => prev + 1);
  };

  useEffect(() => {
    console.log(anonCount);
  }, [anonCount]);

  useEffect(() => {
    console.log(count);
  }, [count]);

  return (
    <>
      <div>
        <button onClick={() => setAnonCount((prev) => prev + 1)}>
          익명함수 더하기
        </button>
        <span>{anonCount}</span>
      </div>
      <div>
        <div>
          <button onClick={handleCount}>기명함수 더하기</button>
          <span>{count}</span>
        </div>
      </div>
    </>
  );
};

export default Example;

2024-09-141 15 55-ezgif com-video-to-gif-converter

두가지 함수의 동작이 같음을 확인할 수 있다

export function App() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    setCount((prev) => prev + 1);
  }, []);

  return <SomeButton onClick={handleClick}>나는버튼</SomeButton>;
}

interface ISomeButton extends PropsWithChildren {
  onClick: () => void;
}

const SomeButton = React.memo(({ children, onClick }: ISomeButton) => {
  console.log("리렌더🚀🚀🚀🚀🚀");
  return <button onClick={onClick}>{children}</button>;
});

그래도 기명함수면 useCallback 등을 써서 최적화를 할 수 있기 때문에 더욱 사용하기 좋다!!(초반에 onClick={() => handleClick()} 처럼 작성하면 SomeButton이 리렌더링될 때마다 새로운 함수를 만들어버려서 React.memo 최적화가 적용되지 않음을 몰랐었다🥲)

🏃‍♀️‍➡️ 더 나아가기

이때, e 혹은 매개변수를 넘겨줘야하는 경우 상위 컴포넌트와 하위 컴포넌트에서 사용할 수 있는 방법은 아래 두가지와 같다

  1. 상위 컴포넌트에서 함수 직접 전달하고 넘기기
const handleClick = (e: React.MouseEvent, num: number) => {
	e.preventDefault();
	setCount((prev) => prev + num);
};

	return <Button onClcik={(e) => handleClick(e, 100)}>나는 버튼</Button>;
};

const Button = ({
	children,
}: {
	children: ReactNode;
	onClick: (e: React.MouseEvent, add: number) => void;
}) => {
	return <button>{children}</button>;
};
  1. 하위 컴포넌트에서 함수를 받아 직접 전달하기
const ButtonEX = () => {
  const [count, setCount] = useState(0);
  const handleClick = (e: React.MouseEvent, num: number) => {
    e.preventDefault();
    setCount((prev) => prev + num);
  };

  return <Button onClick={handleClick}>나는 버튼</Button>;
};

const Button = ({
  children,
  onClick,
}: {
  children: ReactNode;
  onClick: (e: React.MouseEvent, add: number) => void;
}) => {
  return <button onClick={(e) => onClick(e, 100)}>{children}</button>;
};

e와 prev가 넘어가는 차이는?

예시를 보다보면 (e) ⇒ setKeyword(e.target.value)처럼 익명함수의 파라미터로 e가 들어가는 반면에 () => { setCount((a) => a + 1) 는 왜 다를까 하고 궁금할 수 있다.

우선 e(event)는 이벤트 핸들러 함수에서 사용되는 이벤트 객체이다. 우리가 흔히 사용하는 onChange, onClick과 같은 이벤트에서 발생하며 이벤트가 발생한 요소의 정보나 입력 값 등을 갖고있다. 때문에 changeEvent 자체에 대한 객체이므로 사용자가 입력한 값을 가져오기 위해 파라미터로 넘어간다!

예시의 a값은 useState로 관리되는 상태를 업데이트할 때 사용된다. 즉, a라는 state 값의 이전 상태값을 참조하기 위해 사용되는 것과 같은데, a라는 값은 setter가 전달해주는 콜백 구조를 확인할 수 있다

image

useState에 마우스를 올려보면 SetStateAction<S>를 통해

image

사진과 같이 SetStateAction<S>S | ((prevState: S) => S)로 정의되어 있음을 확인할 수 있다. 정리하자면 setCount 함수는 상태 S 자체를 받거나, 이전 상태 값을 인자로 받아 새로운 상태를 반환하는 함수가 받는다는 것을 알 수 있다

  • S는 S 자체가 직접적인 업데이트를 나타내는 값으로 setState(value)처럼 직접 변경되는 경우를 의미한다
  • (prevState:S) ⇒ S 는 상태를 업데이트하는 함수를 의미한다. 이전 상태인 prevState를 인자로 받아 새로운 상태 S를 반환하며, setState((prev) ⇒ prev)와 같이 사용하는 예시가 있다

type을 자세히 뜯어보면 Dispatch는 SetStateAction을 제네릭으로 받아 실행시키는 역할을 한다. 마찬가지로 SetStateAction은 위에 사진에 정의된 바와 같이 useState 상태를 변경하는 함수인 setState에 관련된 타입임 또한 유추할 수 있다 type Dispatch<A> = (value: A) => void;

DIspatch는 결론적으로 외부상태라이브러리를 쓸 때 필요한 인터페이스인데, 다른 곳에 전파를 할 때 사용하는 것과 같다고 한다. 보통 redux, zustand에서 인터페이스로 많이 쓰이며 상태를 변경하면 그 관련된 모든 값들한테 그 상태가 변경되었음을 알려주기 위해 정도로 알고있으면 될 것 같다

<참고> https://velog.io/@hamjw0122/TS-SetStateAction [https://velog.io/@scy0334/201103React-Hooks-정리](https://velog.io/@scy0334/201103React-Hooks-%EC%A0%95%EB%A6%AC) [https://c62-dev.tistory.com/m/entry/Effective-Javascript12장](https://c62-dev.tistory.com/m/entry/Effective-Javascript12%EC%9E%A5)