// File 자체를 저장하는 state
const [files, setFiles] = useState<File[]>([]);
// File을 읽어 미리보기 url을 저장하는 state
const [fileUrls, setFileUrls] = useState<string[]>([]);

function onChangeFiles(file: File, index: number, url: string) {
		// 기존 state들을 담아줍니다.
    const newFiles = [...files];
    const newFileUrls = [...fileUrls];

		// 세개의 버튼 중 이미 사진이 들어있는 곳을 클릭했다면 덮어씌워줍니다.
    if (files[index]) {
      newFiles[index] = file;
      newFileUrls[index] = url;
    } else { // 빈 곳이라면 맨 뒤에 추가해줍니다.
      newFiles.push(file);
      newFileUrls.push(url);
    }

		// 변경된 배열을 state에 저장해줍니다.
    setFiles([...newFiles]);
    setFileUrls([...newFileUrls]);
 }

return (
    <BoardWriteUI
			// ...기존 props들을 적어줍니다.
      onChangeFiles={onChangeFiles}
      fileUrls={fileUrls}
    />
  );
<ImageWrapper>
	<Label>사진첨부</Label>

	// 항상 3개의 컴포넌트가 뿌려집니다.
	// 이미지를 보여줄지, 추가 버튼을 보여줄지는 안에서 조건부로 구분합니다.
	{new Array(3).fill(1).map((data, index) => (
		<Uploads01
			key={`${data}_${index}`}
      index={index}
      onChangeFiles={props.onChangeFiles}
      fileUrls={props.fileUrls}
			/>
	 ))}
</ImageWrapper>
export default function Uploads01(props: IUploads01Props) {
  const fileRef = useRef<HTMLInputElement>(null);
	// 상위 컴포넌트에서 배열 state로 관리하기 때문에
	// 기존에 각각 url을 state로 저장해서 사용했던 부분을 주석처리합니다.
  // const [fileUrl, setFileUrl] = useState("");

  function onClickUpload() {
    fileRef.current?.click();
  }

  async function onChangeFile(event: ChangeEvent<HTMLInputElement>) {
    const file: any = event.target.files?.[0];
    if (!checkValidationFile(file)) return;

    const fileReader = new FileReader();
    fileReader.readAsDataURL(file);
    fileReader.onload = (data) => {
      // setFileUrl(data.target?.result as string);

			// 파일 자체, 부모에서 map으로 뿌려줄 때의 인덱스, 파일을 읽어서 얻은 미리보기 url
      props.onChangeFiles(file, props.index, data.target?.result as string);
    };
  }

  return (
    <Uploads01UI
      fileRef={fileRef}

			// 미리보기 url이 담겨있는 배열에서 해당 위치의 값을 보내줍니다. 
      fileUrl={props.fileUrls[props.index]} 

      onClickUpload={onClickUpload}
      onChangeFile={onChangeFile}
    />
  );
}
export default function Uploads01UI(props: IUploads01UIProps) {
  return (
    <>
			// fileUrls 배열에서 해당 인덱스 위치에 값이 있다면 이미지를 보여주고
			// 없다면 추가 버튼을 보여주게 됩니다.
      {props.fileUrl ? (
        <UploadImage onClick={props.onClickUpload} src={props.fileUrl} />
      ) : (
        <UploadButton onClick={props.onClickUpload}>
          <>+</>
          <>Upload</>
        </UploadButton>
      )}
      <UploadFileHidden
        type="file"
        ref={props.fileRef}
        onChange={props.onChangeFile}
      />
    </>
  );
}

new Array(3).fill(1).map() 을 함으로써 Uploads01 컴포넌트가 3개 그려집니다. 각각 인덱스를 넘겨줍니다. 0, 1, 2가 되겠습니다.

0번째 Uploads01 컴포넌트를 보겠습니다.

fileUrls 라는 state가 있습니다. 초기값은 빈 배열입니다.

fileUrls[0]fileUrls 가 빈 배열이기 때문에 undefined 입니다. fileUrls[0]fileUrl 이라는 이름으로 넘겨줍니다. 이 값을 기준으로 이미지를 보여줄지 추가 버튼을 보여줄지 판별합니다.

props.fileUrl ? <이미지를 보여주는 컴포넌트> : <버튼 추가 컴포넌트> 할 경우 undefined는 false이기 때문에 버튼 추가 컴포넌트를 보여줍니다.