익명함수와 기명함수 넘기기
📝 고민을 하게 된 계기
아래 코드는 프로젝트를 진행하던 중 공지사항 작성 페이지, 작성된 공지 삭제시 뜨는 모달과 관련한 코드이다. 컴포넌트를 정리하다 평상시 무의식적으로 작성했던 부분들에 대해 생각해보니, 그 이유에 대해서는 스스로 제대로 대답하지 못했다. 때문에 이번 기회에 차근차근 헷갈리는 부분을 포함하여 정리해보기로 했다.
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;
두가지 함수의 동작이 같음을 확인할 수 있다
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 혹은 매개변수를 넘겨줘야하는 경우 상위 컴포넌트와 하위 컴포넌트에서 사용할 수 있는 방법은 아래 두가지와 같다
- 상위 컴포넌트에서 함수 직접 전달하고 넘기기
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>;
};
- 하위 컴포넌트에서 함수를 받아 직접 전달하기
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가 전달해주는 콜백 구조를 확인할 수 있다
useState에 마우스를 올려보면 SetStateAction<S>
를 통해
사진과 같이 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) 참고>