TIL - FormItem은 어떻게 바로 자식 컴포넌트에 value, onChange를 넘겨줄까(feat. React.cloneElement)

이종호·2025년 12월 9일

til

목록 보기
1/1

Antd FormItem은 value, onChange를 내려준다.

Antd FormItem은 내부 Input, Select와 같은 컴포넌트에게 value, onChange를 강제로 주입해주는 역할 을 했다.
그래서 바로 아래에서 div같은걸로 감싸면 안되고 무적권 Input이나 Select를 위치시켜야하는 까닭도 그랫다.

이 구조를 알게 되면 이런식으로 컴포넌트를 만들 수 도 있다.

import { Button, Upload } from 'antd';
import { UploadOutlined, DeleteOutlined, PaperClipOutlined } from '@ant-design/icons';
import tw from 'twin.macro';

interface FileInfo {
  id: string;
  name: string;
  url: string;
}

interface FileUploadListProps {
  value?: FileInfo[];
  onChange?: (files: FileInfo[]) => void;
}

export const FileUploadList = ({ value = [], onChange }: FileUploadListProps) => {
  const handleUpload = (info: any) => {
    const { file } = info;

    if (file.status !== 'uploading' && onChange) {
      const newFile = {
        id: Date.now().toString(),
        name: file.name,
        url: URL.createObjectURL(file.originFileObj),
      };
      onChange([...value, newFile]);
    }
  };

  const handleDelete = (index: number) => {
    if (onChange) {
      onChange(value.filter((_, i) => i !== index));
    }
  };

  return (
    <div>
      <Upload onChange={handleUpload} showUploadList={false}>
        <Button icon={<UploadOutlined />}>File Update</Button>
      </Upload>
      <div css={tw`mt-[16px] flex flex-col gap-[8px]`}>
        {value.map((file, index) => (
          <div key={file.id} css={tw`flex items-center justify-between`}>
            <div css={tw`flex items-center gap-[8px]`}>
              <PaperClipOutlined />
              <span css={tw`text-blue-500`}>{file.name}</span>
            </div>
            <Button type="text" icon={<DeleteOutlined />} onClick={() => handleDelete(index)} />
          </div>
        ))}
      </div>
    </div>
  );
};

value와 onChange를 받게 하고, 내부적으로 어떻게 쓰일지 재선언하면 된다.

궁금
어떻게 value, onChange를 명시하지도 않고 내려줄 수 잇는걸까?

React.cloneElement를 사용했다고 한다.

https://ko.react.dev/reference/react/cloneElement

// Ant Design 내부 (간소화한 버전)
const FormItem = ({ children, name, ...props }) => {
  const form = useFormContext(); // Form에서 제공하는 context
  const value = form.getFieldValue(name);
  
  const onChange = (newValue) => {
    form.setFieldValue(name, newValue);
  };

  // 🔑 핵심: React.cloneElement로 props 주입!
  const childWithProps = React.cloneElement(children, {
    value: value,
    onChange: onChange,
  });

  return <div>{childWithProps}</div>;
};
// 원본 컴포넌트
<FileUploadList />

// Form.Item이 내부적으로 변환
React.cloneElement(
  <FileUploadList />,           // 기존 element
  { value: [...], onChange: fn } // 추가할 props
)

// 결과적으로 이렇게 됨
<FileUploadList value={[...]} onChange={fn} />

실제 코드가 정말 그런지 확인해봤다.
https://github.dev/ant-design/ant-design

<Field
  {...props}
  messageVariables={variables}
  trigger={trigger}
  validateTrigger={mergedValidateTrigger}
  onMetaChange={onMetaChange}
>
  {(control, renderMeta, context) => {
    // control 객체가 여기서 전달됨!
  • Field는 @rc-component/form에서 가져온 컴포넌트로, render prop 패턴으로 control 객체를 제공
const childProps: React.ReactElement<any>['props'] = {
  ...mergedChildren.props,  // 기존 props
  ...mergedControl,         // 🔑 control 객체의 모든 속성!
};

// ... (id, aria-* 등 추가)

// 이벤트 핸들러 병합
triggers.forEach((eventName) => {
  childProps[eventName] = (...args: any[]) => {
    mergedControl[eventName]?.(...args);  // Form의 핸들러 먼저
    (mergedChildren as React.ReactElement<any>).props[eventName]?.(...args);  // 원래 props 핸들러
  };
});

// 최종적으로 cloneElement로 적용
childNode = (
  <MemoInput>
    {cloneElement(mergedChildren, childProps)}
  </MemoInput>
);

클로드에게 던져주니, value, onChange 말고도,

const control = {
  value: currentValue,           // 현재 값
  onChange: handleChange,        // 값 변경 핸들러
  onBlur: handleBlur,           // blur 이벤트 (validation trigger)
  onFocus: handleFocus,         // focus 이벤트
  // ... 기타 form control 관련 속성들
};


const childProps = {
  ...mergedChildren.props,        // 원래 있던 props
  ...mergedControl,               // control의 모든 속성 (value, onChange 등)
  
  // 추가 속성들
  id: fieldId,                    // form item id
  'aria-describedby': '...',      // 접근성
  'aria-invalid': 'true',         // 에러 상태
  'aria-required': 'true',        // 필수 필드
  ref: itemRef,                   // ref 전달
};

이렇게 다양한 값들을 넘겨주고 잇으니 필요한 값이 잇으면 받아와서 처리하는 식으로 하면 될거 같다.

profile
코딩은 해봐야 아는 것

0개의 댓글