Callback naming convention for Front-end

이 포스트는 2021년 11월 7일에 발행되었습니다.

들어가기 앞서: 어떤 컨벤션이든 지켜지지 않으면 의미가 없다. 한 번 정한/정해져버린 컨벤션이 있다면 최대한 지키는 것이 좋다. 천천히 마이그레이션 해야지라는 마인드를 절대 적용해선 안된다. 한 번에 싹 바꾸던가, 아니면 기존의 컨벤션을 지키는 것이 맞다.

프론트엔드에서 콜백/리스너 네이밍 컨벤션을 다음과 같이 제안한다.

// example1.js
const MyComponent = () => {
  const handleClick = () => {};
  return (
    <Button
      onClick={handleClick}
    />
  );
};

이는 React의 기본적인 문서만 읽어봐도 알 수 있는 컨벤션이다.

  1. 현재 컴포넌트에서 직접 정의하는 콜백은 handle로 시작한다.
  2. 자식 컴포넌트가 prop으로 받는 콜백은 on으로 시작한다.

따라서, 손자 컴포넌트에게 콜백을 넘겨줄 때는 다음과 같이 써야한다.

// example2.js
const RootComponent = () => {
  const handleClick = () => {};
  return (
    <ParentComponent onClick={handleClick} />
  );
};

const ParentComponent = props => {
  const { onClick } = props;
  return (
    <ChildComponent onClick={onClick} />
  );
};

const ChildComponent = props => {
  const { onClick } = props;
  return (
    <button onClick={onClick}>
      button from child!
    </button>
  );
};

만약에 이를 지키지 않는 경우를 생각해보자.

보통 이렇게 콜백을 단순하게 내려주는 경우도 있지만, 아래 코드와 같이, 부모 컴포넌트에서 받은 콜백과 자기 자신의 콜백을 섞어서 사용하는 경우도 종종 있다.

// example3.js
const RootComponent = () => {
  const handleClick = () => {};
  return (
    <ParentComponent handleClick={handleClick} />
  );
};

const ParentComponent = props => {
  const { handleClick } = props;
  const handleClickMine = () => { // eww
    console.log('hoeeeeng');
    handleClick();
  };
  return (
    <ChildComponent handleClick={handleClickMine} />
  );
};

const ChildComponent = props => {
  const { handleClick } = props;
  return (
    <button onClick={handleClick}>
      button from child!
    </button>
  );
};

위 코드의 line10 (eww)를 보자. prop으로 받아온 콜백과 자기 자신이 직접 정의하는 콜백을 모두 handle시작하는 경우 이런 사태가 벌어지기 십상이다. ChildComponent에서도 콜백을 정의하는 경우, 코드에서 풍기는 악취는 더 독해진다.

따라서 우리는 example3을 멀리하고 example2를 가까이하는 것이 좋다.


하지만 이 규칙과 충돌되는 경우도 존재한다. (특히 bootstrap-react와 같은) 라이브러리에서 자주 보이는 컴포넌트의 구현 패턴인데, 컴포넌트 Caller (컴포넌트의 부모) 에서 컴포넌트의 prop에 값을 바꾸면서 해당 이벤트를 직접 조작해줘야하는 경우이다. 코드로 표현하자면 다음과 같다.

const MyModal = () => {
  const [isModalVisible, setIsModalVisible] = useState(false);
  const showModal = () => { setIsModalVisible(true); };
  const hideModal = () => { setIsModalVisible(false); };
  return (
    <React.Fragment>
      <Button onClick={showModal}>Edit</Button> {/* line 7 */}
      <Modal visible={isModalVisible} onHide={hideModal}>
        ...
      </Modal>
    </React.Fragment>
  );
};

위 코드는 흔히 볼 수 있는 모달 패턴이다. line 7에 보이는 버튼을 눌렀을 때 line 8의 Modal에 주입되는 isModalVisible prop을 통해 모달의 visibility를 컨트롤하고 있다. 이와 같이 콜백에서 이벤트를 조작할 책임이 있는 경우 handle~의 컨벤션은 적합하지 않다고 느꼈다.

굳이 따지자면 맨 처음에 다뤘던 handleClick은 이벤트는 캡슐화된 메커니즘에 의해 (정확히는 브라우저에 의해) 클릭은 알아서 잘 fire가 되었고, 우리는 click 이벤트가 일어났을 때 동작되어야하는 일련의 프로세스를 콜백에 작성했을 뿐이다. 허나 이번에 다룬 show/hideModal은 우리가 직접 메커니즘을 구현/조작하여 이벤트를 fire해야할 책임까지 가지고 있다. 이런 상황에서 같은 컨벤션을 적용하기는 무리가 있다고 생각하여 이런 패턴을 가진 컴포넌트들의 콜백은 다른 컨벤션으로 짜야한다는 생각이다.

덧) 설명의 간결함을 위해 import React from 'react' 라든가, useCallback와 같은 코드는 제외됨.