Navigation

9. 이벤트와 상호작용 흐름

이벤트 핸들러는 정해진 이름을 쓰고 추가 인자는 커링으로 전달합니다. 사용자 액션에 따른 처리는 이펙트가 아닌 핸들러에서 실행합니다.

9.1 핸들러 이름에 handle 접두사를 붙입니다

이벤트 흐름을 이름으로 검색할 수 있습니다.

이벤트 핸들러는 handle 접두사에 대상과 역할을 붙입니다. 표를 위에서부터 읽어 처음 해당하는 형태를 씁니다.

상황이름
DOM 이벤트 객체를 받음handle + Target + Event
이벤트 객체를 받지 않는 도메인 콜백handle + DomainAction

on*은 프롭 이름에만 쓰고, onClick을 처리하는 구현은 handleRowClick처럼 이름 짓습니다. 같은 컴포넌트에서 이름이 겹치지 않도록 대상이 다르면 대상 이름을 넣습니다.

구현에 on*을 쓰고 대상이 이름에 없어 같은 이름이 겹칩니다.

import type {MouseEventHandler} from "react";

// 목록 항목과 저장 버튼 둘 다 클릭을 받는데 이름에 대상이 없어 뒤에 번호가 붙음
/**
 * 이미 고른 항목을 다시 누르면 선택 해제
 */
const onClick: MouseEventHandler<HTMLButtonElement> = (_event) => {
	toggleSelection();
};

/**
 * 폼 기본 제출 차단. 저장은 mutation 콜백이 이어서 처리
 */
const onClick2: MouseEventHandler<HTMLButtonElement> = (event) => {
	event.preventDefault();
};

handle 접두사와 대상, 이벤트가 드러나는 이름을 씁니다.

import type {MouseEventHandler} from "react";

/**
 * 이미 고른 항목을 다시 누르면 선택 해제
 */
const handleListItemClick: MouseEventHandler<HTMLButtonElement> = (_event) => {
	toggleSelection();
};

/**
 * 폼 기본 제출 차단. 저장은 mutation 콜백이 이어서 처리
 */
const handleSaveButtonClick: MouseEventHandler<HTMLButtonElement> = (event) => {
	event.preventDefault();
};

함께 보기

9.2 이벤트 외 추가 인자는 커링으로 넘깁니다

추가 인자 전달만을 위한 JSX 인라인 래퍼를 줄입니다.

onClick, onChange처럼 이벤트 객체를 받는 자리에 추가 인자가 필요하면 커링합니다. 팩토리가 추가 인자를 받고, 안쪽 함수가 이벤트를 받으며, 반환한 함수를 JSX에 직접 전달합니다.

flowchart LR
	q1{"이벤트 객체를<br>받는 프롭인가?"} -- 예 --> q2{"이벤트 밖 인자가<br>더 필요한가?"} -- 예 --> r2("커링 팩토리를 JSX에 전달")
	q1 -- 아니요 --> r1("이름 붙인 핸들러를 그대로 전달")
	q2 -- 아니요 --> r1
작성할 부분기준
팩토리 이름커링 없는 핸들러처럼 handle*을 씁니다. 호출 인자로 용도를 알 수 있으므로 With<인자>는 붙이지 않습니다
반환 타입규칙 3.1에 따라 React 별칭을 씁니다
함수 형태화살표 두 단계로 적고, 안쪽 함수에 별도 이름을 붙여 반환하지 않습니다
반환 전 준비 계산이 있음이때만 바깥 블록 본문을 엽니다. 반환하는 화살표는 이름을 붙이지 않습니다
이벤트를 받지 않는 (id) => void 프롭 콜백커링하지 않고 이름 붙인 핸들러를 그대로 넘깁니다
useEffectEvent 반환 함수DOM 이벤트 매개변수나 커링을 덧붙이지 않습니다

인라인 래퍼로 인자를 넘깁니다.

<UiButton onClick={() => handleListItemClick(product.id)}>{product.name}</UiButton>;

JSX에는 팩토리 호출만 두고 감싸는 화살표를 만들지 않습니다.

<UiButton onClick={handleListItemClick(product.id)}>{product.name}</UiButton>;

안쪽 핸들러에 별도 이름을 붙이고 팩토리에 With 접미사를 붙입니다.

const handleListItemClickWithProductId = (productId: string): MouseEventHandler<HTMLButtonElement> => {
	const handleListItemClick: MouseEventHandler<HTMLButtonElement> = (_event) => {
		toggleSelection(productId);
	};

	return handleListItemClick;
};

추가 인자는 바깥 함수, 이벤트는 안쪽 함수입니다.

import type {MouseEventHandler} from "react";

/**
 * 클릭한 항목을 이벤트 대신 팩토리 인자로 받아 어느 상품인지 식별
 */
const handleListItemClick =
	(productId: string): MouseEventHandler<HTMLButtonElement> =>
	(_event) => {
		toggleSelection(productId);
	};

함께 보기

9.3 사용자 액션은 이펙트가 아니라 핸들러에서 실행합니다

사용자 액션이 무관한 이펙트 재실행으로 반복되는 것을 막습니다.

제출, 저장, 삭제, 닫기 같은 사용자 액션은 해당 핸들러 안에서 바로 실행합니다. 액션 자체를 상태로 올린 뒤 useEffect가 나중에 실행하게 만들지 않습니다.

사용자 액션을 상태 + 이펙트로 모델링합니다.

/**
 * 생성 성공 시 목록으로 이동
 */
const mutationProductCreate = useProductCreate({
	mutation: {
		onSuccess: () => {
			void navigate("/products");
		},
	},
});

const [shouldSubmit, setShouldSubmit] = useState(false);

useEffect(() => {
	if (!shouldSubmit) {
		return;
	}

	mutationProductCreate.mutate({data: toProductCreateRequest(formValues)});
}, [mutationProductCreate, formValues, shouldSubmit]);

const handleSaveButtonClick: MouseEventHandler<HTMLButtonElement> = (_event) => {
	setShouldSubmit(true);
};

사용자 액션은 핸들러 안에서 바로 실행합니다.

/**
 * 생성 성공 시 목록으로 이동
 */
const mutationProductCreate = useProductCreate({
	mutation: {
		onSuccess: () => {
			void navigate("/products");
		},
	},
});

/**
 * 버튼을 누른 그 자리에서 생성 호출. 상태로 올려 이펙트가 대신 부르게 하지 않음
 */
const handleSaveButtonClick: MouseEventHandler<HTMLButtonElement> = (_event) => {
	mutationProductCreate.mutate({data: toProductCreateRequest(formValues)});
};