[๐Ÿถ ์‚ฌ์ด๋“œ ํ”„๋กœ์ ํŠธ] ios ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ์—์„œ onBlur๊ฐ€ ์ œ๋Œ€๋กœ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š” ์ด์Šˆ

JEยท2024๋…„ 10์›” 27์ผ
1

์‹œ์ž‘

๊ฒฌ์ฃผ(์œ ์ €)๊ฐ€ ๊ฐ€์ž…์‹ ์ฒญ ์Šน์ธ ์ดํ›„ ํ”„๋กœํ•„ ์„ค์ • ํŽ˜์ด์ง€๋กœ ๋„˜์–ด๊ฐ€๊ฒŒ ๋œ๋‹ค.

๋‚ด๊ฐ€ ๊ธฐ๋Œ€ํ•˜๋Š” ์•ก์…˜ ํ”Œ๋กœ์šฐ

  1. ์ด๋ฏธ์ง€ ์ธ๋„ค์ผ์ด ํ‘œ์‹œ๋˜๋Š” ์˜์—ญ ํด๋ฆญ
  2. ๋ฒ„ํŠผ UI ํ™œ์„ฑํ™” (active ์ƒํƒœ)
  3. ๋ฒ„ํŠผ UI ํ™œ์„ฑํ™” ์ƒํƒœ์—์„œ ๋‹ค์‹œ ํด๋ฆญ ์‹œ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์ฐฝ ๋„์›Œ์ง€๊ธฐ
์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์‹œ์ด๋ฏธ์ง€ ์„ ํƒ ์‹œ

๋ฐœ์ƒํ•œ ์ด์Šˆ

ios ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ์—์„  active UI๋Š” ํ‘œ์‹œ ๋˜๋Š”๋ฐ ์ด๋ฅผ ํด๋ฆญ ํ•  ๊ฒฝ์šฐ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์ฐฝ์ด ๋„์›Œ์ง€์ง€ ์•Š์•˜๋‹ค.
ํ•ด๋‹น ํŽ˜์ด์ง€๋Š” ๊ฐ€์ž…์‹ ์ฒญ ์ ˆ์ฐจ์—์„œ ๋ฌด์กฐ๊ฑด ์ง„ํ–‰ ๋˜์–ด์•ผ ํ•˜๋Š” ํ”Œ๋กœ์šฐ๋กœ ์ด์Šˆ๋ฅผ ํ™•์ธ ํ•˜๊ณ  ๋น ๋ฅด๊ฒŒ ํ•ด๊ฒฐํ–ˆ์–ด์•ผ ํ–ˆ๋‹ค.

๋‹ค๋งŒ ๋‹ค๋ฅธ ํ™˜๊ฒฝ์—์„  ์ž˜ ๋˜๋Š”๋ฐ ios ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ์—์„œ ์ ์šฉ์ด ์•ˆ ๋˜๊ณ  ์žˆ๋Š” ์ƒํ™ฉ์ด์˜€๋‹ค.

1. ios ํ™˜๊ฒฝ์—์„œ onBlur ์ด์Šˆ

์›๋ž˜๋ผ๋ฉด button์ด focus๋˜๋ฉด active UI๊ฐ€ ํ‘œ์‹œ๋˜๊ณ  ๋‹ค๋ฅธ ๊ณณ ํ„ฐ์น˜(onBlur) ํ•  ๊ฒฝ์šฐ ํ•ด๋‹น active UI๊ฐ€ ํ’€๋ ค์•ผ ํ•œ๋‹ค.

ios ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ์—์„œ onBlur๊ฐ€ ์ œ๋Œ€๋กœ ์ ์šฉ๋˜์ง€ ์•Š๋Š” ์ด์Šˆ๊ฐ€ ์žˆ์—ˆ๋‹ค.

iosํ™˜๊ฒฝ์—์„œ onBlur ์ด์Šˆ ๊ด€๋ จ ๋ ˆํผ๋Ÿฐ์Šค

https://stackoverflow.com/questions/61245883/why-blur-and-focus-doesnt-work-on-safari

https://stackoverflow.com/questions/13492881/why-is-blur-event-not-fired-in-ios-safari-mobile-iphone-ipad?rq=3

๊ตฌ๊ธ€๋ง ํ•ด๋ณด๋‹ˆ ios ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ์—์„œ๋Š” ๋‹ค๋ฅธ ์˜์—ญ์„ ํ„ฐ์น˜ํ•ด๋„ focus๋Š” ์•„์ง ์ด์ „ ์˜์—ญ์— ๋จธ๋ฌผ๋Ÿฌ ์žˆ์–ด blur๊ฐ€ ์ œ๋Œ€๋กœ ๋™์ž‘ํ•˜์ง€ ์•Š์„ ์ˆ˜๋„ ์žˆ๋‹ค๊ณ  ํ•œ๋‹ค.

ํ”„๋กœํ•„ ์„ค์ •์€ ProfileUploadBox ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ด€๋ฆฌ๋˜๊ณ  ์žˆ๋‹ค.

ํ”„๋กœํ•„ ์ˆ˜์ •, ๋“ฑ๋ก์œผ๋กœ ๋‚˜๋‰˜๊ฒŒ ๋˜์ง€๋งŒ ๋™์ผํ•œ ๊ธฐ๋Šฅ์„ ํ•˜๊ณ  ์žˆ์–ด ๊ด€๋ฆฌํ•˜๊ธฐ ํŽธ๋ฆฌํ•˜๋„๋ก ๋ฌถ์–ด ๋‘์—ˆ๋‹ค.
ํ˜„์žฌ ๋‹ค์–‘ํ•œ ํ™˜๊ฒฝ์„ ์ƒ๊ฐํ•˜์ง€ ์•Š๊ณ  ๊ตฌ์กฐ๋ฅผ ์ž‘์„ฑ + ๋ถˆํ•„์š”ํ•œ ์ฝ”๋“œ๊ฐ€ ๋งŽ์•„ ์ฝ”๋“œ ๊ฐœ์„ ์ด ํ•„์š”ํ–ˆ๋‹ค.

// ProfileUploadBox.tsx ์ˆ˜์ • ์ „
type Mode = "create" | "edit";

interface ProfileUploadProps {
  type: string;
  isActive?: boolean;
  setIsActive?: React.Dispatch<React.SetStateAction<boolean>>;
  fileRef?: React.RefObject<HTMLInputElement>;
  fileName: string;
  mode: Mode;
  onClick?: () => void;
}

const ProfileUploadBox = ({
  type,
  isActive,
  setIsActive,
  fileRef,
  fileName,
  mode,
  onClick
}: ProfileUploadProps) => {
  const { setValue } = useFormContext();
  const [profile, setProfile] = useState<IFile[]>([]);

  const handleClick = () => {
    if (fileRef && fileRef.current) {
      mode === "create" && isActive ? setIsActive && setIsActive(false) : fileRef.current.click();
    }
    onClick && onClick();
  };

  const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
    const FileList = e.target.files;

    if (!FileList) {
      showToast("์—…๋กœ๋“œํ•  ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค.", "ownerNav");
      return;
    }

    // ํŒŒ์ผ ๋ณ€๊ฒฝ ์—†์„ ๊ฒฝ์šฐ
    if (FileList.length <= 0) {
      if (!isActive && setIsActive) setIsActive(true);
      return;
    }

    if (FileList) {
      const newFiles = Array.from(FileList);
      const fileArray = await Promise.all(newFiles.map(getFilePreview));

      setProfile([...fileArray]);
      setValue(fileName, [...newFiles], { shouldValidate: true });
      if (mode === "create" && setIsActive) setIsActive(true);
    }
  };

  return (
    <>
      {mode === "create" && (
        <ProfileCreate
          isActive={isActive}
          setIsActive={setIsActive}
          profile={profile}
          fileInputRef={fileRef}
          handleFileChange={handleFileChange}
          handleClick={handleClick}
          registerText={fileName}
          type={type}
        />
      )}
      {mode === "edit" && (
        <ProfileEdit
          profile={profile}
          handleFileChange={handleFileChange}
          handleClick={handleClick}
          registerText={fileName}
          type={type}
        />
      )}
    </>
  );
};

export default ProfileUploadBox;
// ProfileCreate ์ˆ˜์ • ์ „
const ProfileCreate = ({
  isActive,
  setIsActive,
  profile,
  fileInputRef,
  handleFileChange,
  registerText,
  type
}: ProfileCreateProps) => {
  const divRef = useRef<HTMLButtonElement>(null);
  const { register } = useFormContext();
  const handleClickTarget = () => {
    divRef?.current?.focus();
  };

  const handleActive = () => {
    isActive && setIsActive?.(false);
  };

  return (
    <Flex align="center" direction="column" justify="center" gap="12" width="100%">
      <S.ProfileBox>
        <S.UploadProfileButton
          id={type}
          onClick={handleClickTarget}
          onBlur={() => setIsActive?.(true)}
          ref={divRef}
          aria-label="uploadProfileButton"
        >
          {profile.length > 0 ? (
            <>
              <S.UploadImage
                onClick={handleActive}
                src={profile[0].thumbnail}
                alt={`${type}-profile`}
              />
              {!isActive && <ProfileActiveBox htmlFor={registerText} />}
            </>
          ) : (
            <>
              <S.ProfileLabel htmlFor={registerText} />
              <AddCIcon />
            </>
          )}
        </S.UploadProfileButton>
      </S.ProfileBox>

      <S.StyledHiddenUpload
        {...register(registerText, {
          required: true,
          onChange: (e) => handleFileChange(e, type)
        })}
        ref={fileInputRef}
        id={registerText}
        type="file"
        accept={ACCEPT_FILE_TYPE.IMAGE}
      />
    </Flex>
  );
};

1) ios ํ™˜๊ฒฝ์œผ๋กœ ํ™•์ธ์„ ์–ด๋–ป๊ฒŒ ํ•˜์ง€?

ํ•ด๋‹น ์ด์Šˆ๋Š” PC ํ™˜๊ฒฝ์ด ์•„๋‹Œ Mobile ํ™˜๊ฒฝ์œผ๋กœ ์›น์—์„  ํ™•์ธํ•  ๋ฐฉ๋ฒ•์ด ์—†์—ˆ๋‹ค.

๋งฅ๋ถ์˜ Xcode ํ”„๋กœ๊ทธ๋žจ์„ ํ†ตํ•ด ํ™•์ธ ํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์ง€๋งŒ
๋‚˜์˜ ๋…ธํŠธ๋ถ์€ ๊ฑฐ์˜ 6๋…„์ด ๋„˜์–ด์„œ ๊ทธ๋Ÿฐ์ง€... Xcode ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋ฒ„ํ‹ฐ์งˆ ๋ชปํ–ˆ๋‹ค..ใ… 

๊ทธ๋Ÿฌ๋‹ค ํœด๋Œ€ํฐ์—์„œ๋„ local ํ™˜๊ฒฝ์„ ํ™•์ธ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„ ๋ƒˆ๊ณ 
์ฝ”๋“œ ์ˆ˜์ • ์‹œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐ˜์˜๋˜์–ด ๋‚˜์ค‘์—๋„ ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ ์ž‘์—…์„ ํ•  ๋•Œ๋„ ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ๊ฒƒ ๊ฐ™๋‹ค.

์•„์ดํฐ์—์„œ local ํ™˜๊ฒฝ ๋„์šฐ๊ธฐ

  1. ๋งฅ๋ถ, ์•„์ดํฐ์ด ๋™์ผํ•œ ์™€์ดํŒŒ์ด๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š”์ง€ ํ™•์ธ
  2. ๋งฅ๋ถ์— ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๋Š” ์™€์ดํŒŒ์ด ์•„์ดํ”ผ ์ฃผ์†Œ ํ™•์ธ
  3. ์•„์ดํ”ผ์ฃผ์†Œ:3000 ์„ ์•„์ดํฐ ์‚ฌํŒŒ๋ฆฌ ์ฃผ์†Œ์ฐฝ์— ์ž…๋ ฅ

๋ชจ๋ฐ”์ผ ๊ธฐ๊ธฐ์—์„œ localhost ์ ‘์†ํ•˜๋Š” ๋ฐฉ๋ฒ•

2) ios ํ™˜๊ฒฝ์—์„œ์˜ ๋ฌธ์ œ์  ํ™•์ธ

button์˜ onBlur={() => setIsActive?.(true)} ๊ฐ€ ์ •ํ™•ํžˆ ๋™์ž‘ํ•˜์ง€ ์•Š์•„ ProfileActiveBox๊ฐ€ ํ™œ์„ฑํ™” ๋˜์–ด๋„ file upload ํ™”๋ฉด์„ ๋„์šฐ์ง€ ์•Š๋Š” ์ ์ด์˜€๋‹ค.

onBlur๋กœ ํ–ˆ๋˜ ์ด์œ ๋Š” ์ตœ๋Œ€ํ•œ ํ•จ์ˆ˜๋ฅผ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ  ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ์ž‘์—… ํ–ˆ์—ˆ๋‹ค.

์ „๋‹ฌํ•˜๋Š” ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ๋ณ€๊ฒฝ

์šฐ์„  isActive์ธ boolean state์™€ onBlur๋กœ ๊ด€๋ฆฌํ•˜๋˜ ์ฝ”๋“œ ๋ถ€ํ„ฐ ์ˆ˜์ •ํ–ˆ๋‹ค.

isActive๋ฅผ ํ†ตํ•ด ProfileActiveBox๋ฅผ ํ™œ์„ฑํ™” ๋˜๋„๋ก ํ•˜๋˜ ๋ถ€๋ถ„์„
ํ˜„์žฌ ํด๋ฆญํ•œ(active๋œ) ๋ฐ์ดํ„ฐ๊ฐ€ ํ•ด๋‹น type๊ณผ ์ผ์น˜ํ•  ๊ฒฝ์šฐ boolean ๊ฐ’์œผ๋กœ ํ‘œ์‹œํ•˜์—ฌ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ํ•ด๋‹น ์˜์—ญ๋งŒ ํ™œ์„ฑํ™”(true) ๋˜๋„๋ก ์ „๋‹ฌํ•˜๋Š” ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ๋ณ€๊ฒฝ ํ–ˆ๋‹ค.

// ๋ฐ์ดํ„ฐ ๋„˜๊ฒจ ์ค„ ๋•Œ (์ˆ˜์ • ์ „)
const [isMyActive, setIsMyActive] = useState(false);
const [isDogActive, setIsDogActive] = useState(false);
const myFileInputRef = useRef<HTMLInputElement>(null);
const dogFileInputRef = useRef<HTMLInputElement>(null);

  const profileDatas = [
    {
      type: TYPE_NAME.MEMBER,
      isActive: isMyActive,
      setIsActive: setIsMyActive,
      fileName: FILE_NAME.PROFILE_MEMBER,
      fileRef: myFileInputRef
    },
    {
      type: TYPE_NAME.DOG,
      isActive: isDogActive,
      setIsActive: setIsDogActive,
      fileName: FILE_NAME.PROFILE_DOG,
      fileRef: dogFileInputRef
    }
  ];

// ๋ฐ์ดํ„ฐ ๋„˜๊ฒจ ์ค„ ๋•Œ (์ˆ˜์ • ํ›„)
const [activeType, setActiveType] = useState("");
const myFileInputRef = useRef<HTMLInputElement>(null);
const dogFileInputRef = useRef<HTMLInputElement>(null);

const profileDatas = [
    {
      type: TYPE_NAME.MEMBER,
      isType: activeType === TYPE_NAME.MEMBER,
      fileName: FILE_NAME.PROFILE_MEMBER,
      fileRef: myFileInputRef
    },
    {
      type: TYPE_NAME.DOG,
      isType: activeType === TYPE_NAME.DOG,
      fileName: FILE_NAME.PROFILE_DOG,
      fileRef: dogFileInputRef
    }
  ];

์ „๋‹ฌํ•˜๋Š” ๋ฐ์ดํ„ฐ๊ตฌ์กฐ ๋ณ€๊ฒฝ์— ๋”ฐ๋ผ ์ฝ”๋“œ ์ˆ˜์ •ํ•˜๊ธฐ

๋˜ํ•œ ๊ธฐ์กด์— onBlur๋ฅผ ์‚ฌ์šฉํ•˜๋˜ ๋ฒ„ํŠผ์˜ ์†์„ฑ์„ ์ „๋ถ€ ์ง€์šฐ๊ณ 
์‹ค์ œ๋กœ ํด๋ฆญํ•ด์•ผ ํ•˜๋Š” ์˜์—ญ์— onClicke ์ด๋ฒคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด
๋งŒ์•ฝ true์ธ ๊ฒฝ์šฐ ProfileActiveBox๊ฐ€ ํ™œ์„ฑํ™” ๋˜๋„๋ก ์ˆ˜์ •ํ–ˆ๋‹ค.

const handleClickTarget = (currentType: string) => {
    setActiveType?.(currentType);
  };

return (
    <Flex align="center" direction="column" justify="center" gap="12" width="100%">
      <S.ProfileBox id={type}>
        <S.UploadProfileButton aria-label="uploadProfileButton">
          {profile.length > 0 ? (
            <>
              {isActiveType && (
                <S.ProfileLabel htmlFor={registerText} ref={labelRef}>
                  <ProfileActiveBox />
                </S.ProfileLabel>
              )}
              <S.UploadImage
                onClick={() => handleClickTarget(type)}
                src={profile[0].thumbnail}
                alt={`${type}-profile`}
              />
            </>
          ) : (
            <>
              <S.ProfileLabel htmlFor={registerText} />
              <AddCIcon />
            </>
          )}
        </S.UploadProfileButton>
      </S.ProfileBox>

      <S.StyledHiddenUpload
        {...register(registerText, {
          required: true,
          onChange: (e) => {
            handleFileChange(e, type);
          }
        })}
        ref={fileInputRef}
        id={registerText}
        type="file"
        accept={ACCEPT_FILE_TYPE.IMAGE}
      />
    </Flex>
  );

ProfileActiveBox์— ProfileLabel๋ฅผ ๊ฐ์‹ผ ์ด์œ 

์ตœ๋Œ€ํ•œ ํƒœ๊ทธ ๋ณธ์—ฐ์˜ ๊ธฐ๋Šฅ์„ ์‚ด๋ฆฌ๊ณ  ์‹ถ์—ˆ๋‹ค.
label ํƒœ๊ทธ ์†์„ฑ์˜ htmlFor๊ณผ input ํƒœ๊ทธ ์†์„ฑ์˜ id ๊ฐ’์„ ๋™์ผํ•˜๊ฒŒ ํ•ด๋„ ํ•ด๋‹น ์˜์—ญ ํด๋ฆญ ์‹œ ํ™œ์„ฑํ™” ๋œ๋‹ค.

๊ตณ์ด onClick ์ด๋ฒคํŠธ์™€ ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•  ํ•„์š”๊ฐ€ ์—†๊ธฐ์— label ํƒœ๊ทธ๋กœ ํ•ด๊ฒฐํ–ˆ๋‹ค..

2. ๋˜ ๋‹ค๋ฅธ ๋ฌธ์ œ ๋ฐœ์ƒ

1) active๋Š” ์ œ๋Œ€๋กœ ๋˜๋Š”๋ฐ ๋‹ค๋ฅธ ์˜์—ญ ํด๋ฆญํ•ด๋„ ๊ณ„์† active ์ƒํƒœ ๋˜๋Š” ์ด์Šˆ

ios ํ™˜๊ฒฝ์—์„  mousedown์ด focus์™€ ๋น„์Šทํ•˜๊ฒŒ ์ž‘์šฉ ๋œ๋‹ค๊ณ  ํ•œ๋‹ค.

const labelRef = useRef<HTMLLabelElement>(null);

const handleClickOutside = useCallback(
    (e: MouseEvent) => {
      const labelTarget = labelRef?.current;
      const inside = labelTarget?.contains(e.target as Node);

      if (inside) return;
      // ์™ธ๋ถ€ ํด๋ฆญ ์‹œ ๋ชจ๋“  ํ™œ์„ฑํ™” ํ•ด์ œ
      if (labelTarget && !inside) {
        setActiveType?.("");
      }
    },
    [setActiveType]
  );

  useEffect(() => {
    document.addEventListener("mousedown", handleClickOutside);

    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, [handleClickOutside, setActiveType]);

๋งŒ์•ฝ labelTarget ๋ง๊ณ  ๋‹ค๋ฅธ ์˜์—ญ ํด๋ฆญ ์‹œ setActiveType์— ๋นˆ ๋ฌธ์ž์—ด์„ ์ฃผ์–ด ๋น„ํ™œ์„ฑํ™” ๋˜๋„๋ก ํ–ˆ๋‹ค.

2) ํŒŒ์ผ ์—…๋กœ๋“œ ์ƒํƒœ์—์„œ "์ทจ์†Œ" ๋ฒ„ํŠผ ํด๋ฆญ ํ•  ๊ฒฝ์šฐ ์ด์Šˆ

ํŒŒ์ผ ์—…๋กœ๋“œ ์‹œ "์ทจ์†Œ" ๋ฒ„ํŠผ ํด๋ฆญ ํ•  ๊ฒฝ์šฐ ๊ณ„์† active ์ƒํƒœ๊ฐ€ ๋˜์–ด ์žˆ์—ˆ๋‹ค.

์ด๋• focus ์ด๋ฒคํŠธ๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๋œ๋‹ค๊ณ  ํ•œ๋‹ค.
์•„๋งˆ ์ทจ์†Œ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ํฌ์ปค์Šค๊ฐ€ ํ•ด๋‹น ํŽ˜์ด์ง€(window)๋กœ ์ดˆ์ ์ด ๋งž์ถฐ์ง€๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ€๋Šฅํ•˜์ง€ ์•Š๋‚˜ ์ถ”์ธกํ•ด ๋ณธ๋‹ค.

๋‹ค๋งŒ ios ํ™˜๊ฒฝ์—์„œ๋Š” focus ์ด๋ฒคํŠธ๊ฐ€ ์ œ๋Œ€๋กœ ๋™์ž‘ํ•˜๊ณ  ์žˆ์ง€ ์•Š์•„์„œ ์ด์ ์€ ๋‹ค์‹œ ํ™•์ธ์ด ํ•„์š”ํ•  ๊ฒƒ ๊ฐ™๋‹ค...ใ… 

// ProfileUploadBox.tsx
const handleClick = () => {
    if (fileRef && fileRef.current) {
      mode === "create" && isActiveType ? setActiveType?.("") : fileRef.current.click();
    }
    onClick && onClick();
  };

  const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
    const FileList = e.target.files;

    if (!FileList) {
      showToast("์—…๋กœ๋“œํ•  ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค.", "bottom");
      return;
    }

    // ํŒŒ์ผ ์„ ํƒ ํ›„ "์ทจ์†Œ" ํด๋ฆญ ์‹œ
    if (FileList.length === 0) {
      setActiveType?.("");
      return;
    }

    if (FileList) {
      const newFiles = Array.from(FileList);
      const fileArray = await Promise.all(newFiles.map(getFilePreview));

      setProfile([...fileArray]);
      setValue(fileName, [...newFiles], { shouldValidate: true });
      if (mode === "create") setActiveType?.("");
    }
  };

  useEffect(() => {
    // FIXME ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ focus ๋Œ€์ฒด ์ด๋ฒคํŠธ ํ™•์ธ ํ›„ ์ˆ˜์ • ํ•„์š”
    // ํŒŒ์ผ ๋ฏธ์„ ํƒ ํ›„ "์ทจ์†Œ" ํด๋ฆญ ์‹œ
    window.addEventListener("focus", () => setActiveType?.(""));
    return () => {
      window.removeEventListener("focus", () => setActiveType?.(""));
    };
  }, [setActiveType]);

๊ฐœ์„ ๋œ ์ฝ”๋“œ

// ProfileUploadBox.tsx
const ProfileUploadBox = ({
  type,
  fileRef,
  fileName,
  mode,
  onClick,
  isActiveType,
  setActiveType
}: ProfileUploadProps) => {
  const { setValue } = useFormContext();
  const [profile, setProfile] = useState<FileType[]>([]);

  const handleClick = () => {
    if (fileRef && fileRef.current) {
      mode === "create" && isActiveType ? setActiveType?.("") : fileRef.current.click();
    }
    onClick && onClick();
  };

  const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
    const FileList = e.target.files;

    if (!FileList) {
      showToast("์—…๋กœ๋“œํ•  ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค.", "bottom");
      return;
    }

    // ํŒŒ์ผ ์„ ํƒ ํ›„ "์ทจ์†Œ" ํด๋ฆญ ์‹œ
    if (FileList.length === 0) {
      setActiveType?.("");
      return;
    }

    if (FileList) {
      const newFiles = Array.from(FileList);
      const fileArray = await Promise.all(newFiles.map(getFilePreview));

      setProfile([...fileArray]);
      setValue(fileName, [...newFiles], { shouldValidate: true });
      if (mode === "create") setActiveType?.("");
    }
  };

  useEffect(() => {
    // FIXME ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ focus ๋Œ€์ฑ„ ์ด๋ฒคํŠธ ํ™•์ธ ํ›„ ์ˆ˜์ • ํ•„์š”
    // ํŒŒ์ผ ๋ฏธ์„ ํƒ ํ›„ "์ทจ์†Œ" ํด๋ฆญ ์‹œ
    window.addEventListener("focus", () => setActiveType?.(""));
    return () => {
      window.removeEventListener("focus", () => setActiveType?.(""));
    };
  }, [setActiveType]);

  return (
    <>
      {mode === "create" && (
        <ProfileCreate
          profile={profile}
          fileInputRef={fileRef}
          registerText={fileName}
          type={type}
          isActiveType={isActiveType}
          setActiveType={setActiveType}
          handleFileChange={handleFileChange}
          handleClick={handleClick}
        />
      )}
      {mode === "edit" && (
        <ProfileEdit
          profile={profile}
          registerText={fileName}
          type={type}
          handleFileChange={handleFileChange}
          handleClick={handleClick}
        />
      )}
    </>
  );
};

export default ProfileUploadBox;
// ProfileCreate.tsx
const ProfileCreate = ({
  profile,
  fileInputRef,
  registerText,
  type,
  isActiveType,
  setActiveType,
  handleFileChange
}: ProfileCreateProps) => {
  const labelRef = useRef<HTMLLabelElement>(null);
  const { register } = useFormContext();

  const handleClickTarget = (currentType: string) => {
    setActiveType?.(currentType);
  };

  const handleClickOutside = useCallback(
    (e: MouseEvent) => {
      const labelTarget = labelRef?.current;
      const inside = labelTarget?.contains(e.target as Node);

      if (inside) return;
      // ์™ธ๋ถ€ ํด๋ฆญ ์‹œ ๋ชจ๋“  ํ™œ์„ฑํ™” ํ•ด์ œ
      if (labelTarget && !inside) {
        setActiveType?.("");
      }
    },
    [setActiveType]
  );

  useEffect(() => {
    document.addEventListener("mousedown", handleClickOutside);

    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, [handleClickOutside, setActiveType]);

  return (
    <Flex align="center" direction="column" justify="center" gap="12" width="100%">
      <S.ProfileBox id={type}>
        <S.UploadProfileButton aria-label="uploadProfileButton">
          {profile.length > 0 ? (
            <>
              {isActiveType && (
                <S.ProfileLabel htmlFor={registerText} ref={labelRef}>
                  <ProfileActiveBox />
                </S.ProfileLabel>
              )}
              <S.UploadImage
                onClick={() => handleClickTarget(type)}
                src={profile[0].thumbnail}
                alt={`${type}-profile`}
              />
            </>
          ) : (
            <>
              <S.ProfileLabel htmlFor={registerText} />
              <AddCIcon />
            </>
          )}
        </S.UploadProfileButton>
      </S.ProfileBox>

      <S.StyledHiddenUpload
        {...register(registerText, {
          required: true,
          onChange: (e) => {
            handleFileChange(e, type);
          }
        })}
        ref={fileInputRef}
        id={registerText}
        type="file"
        accept={ACCEPT_FILE_TYPE.IMAGE}
      />
    </Flex>
  );
};

export default ProfileCreate;

์ด์ „ ์ฝ”๋“œ์™€ ๋น„๊ต ํ–ˆ์„ ๋•Œ ์ฝ”๋“œ๊ฐ€ ๊ฐœ์„ ๋œ๊ฑธ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.
๋ฌด์—‡ ๋ณด๋‹ค ์‚ฌ์šฉ์ž ์ž…์žฅ์œผ๋กœ ์ƒ๊ฐํ•ด ์—ฌ๋Ÿฌ ๋ณ€์ˆ˜์˜ QA๋ฅผ ์ง„ํ–‰ํ•˜๋ฉฐ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋„ ๊ฐ™์ด ์ž‘์—…ํ–ˆ๋‹ค.

์•„์‰ฌ์šด ์ ์€ ios ํ™˜๊ฒฝ์—์„œ ์ด๋ฏธ์ง€ ๋ฏธ์„ ํƒ ์ƒํƒœ๋กœ "์ทจ์†Œ" ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ focus ๋น„ํ™œ์„ฑํ™”๋ฅผ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•ด์•ผํ• ์ง€,
๊ทธ๋ฆฌ๊ณ  props๋ฅผ 2๋ฒˆ ๋‚ด๋ ค ๋ฐ›๋Š” ๊ตฌ์กฐ์ธ๋ฐ ์ด๊ฒŒ ๋งž๋Š”์ง€ ๊ณ ๋ฏผ์ด๋‹ค.

๋˜ํ•œ useForm์€ ref๋ฅผ ๊ธฐ๋ณธ์œผ๋กœ ์ œ๊ณตํ•˜๊ณ  ์žˆ์–ด์„œ
๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์‹œ ref ๋ฐ์ดํ„ฐ๊ฐ€ ๊ตณ์ด ์žˆ์–ด์•ผ ํ•˜๋‚˜ ์‹ถ๊ธฐ๋„ ํ•œ๋‹ค.

ํ•ญ์ƒ ์žฌ์‚ฌ์šฉ์„ฑ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค ๋•Œ
ํ•ด๋‹น ๊ธฐ๋Šฅ์ด 2๋ฒˆ ์ด์ƒ ๋™์ผํ•œ ๊ธฐ๋Šฅ์œผ๋กœ ์‚ฌ์šฉ๋˜๊ณ  ์žˆ๋Š”์ง€?๋ฅผ ์ƒ๊ฐํ•˜๋ฉด์„œ ์ž‘์—…ํ•˜๊ณ  ์žˆ์ง€๋งŒ...
๊ตฌ์กฐ์— ๋Œ€ํ•ด์„  ์ข€ ๋” ๊นŠ๊ฒŒ ๊ณ ๋ฏผํ•ด์•ผ ๋‚˜์ค‘์— ์œ ์ง€๋ณด์ˆ˜ ์ธก๋ฉด์—์„œ๋„ ์ข‹์ง€ ์•Š์„๊นŒ ์‹ถ๋‹ค.

์ง€๊ธˆ์€ ๊ธ‰ํ•ด์„œ ์ด์Šˆ ์‚ฌํ•ญ๋งŒ ํ•ด๊ฒฐ ํ–ˆ๋Š”๋ฐ ๋‚˜์ค‘์— ๋ฆฌํŒฉํ† ๋ง ์ž‘์—…์„ ํ•ด์•ผ๊ฒ ๋‹ค.


๋งˆ์น˜๋ฉฐ

์ด์Šˆ๋ฅผ ํ•ด๊ฒฐํ•˜๊ณ  ์ด๋ฅผ ๋ธ”๋กœ๊ทธ๋กœ ์ž‘์„ฑํ•˜๋ ค๊ณ  ํ•˜๋‹ˆ ์ œ๋Œ€๋กœ ์ •๋ฆฌ๊ฐ€ ์•ˆ ๋˜๋Š” ๊ธฐ๋ถ„์ด๋‹ค...

ํ˜ผ์ž ๋ณผ ๋• ์ดํ•ด๊ฐ€ ๋˜์ง€๋งŒ ๋‹ค๋ฅธ ์‚ฌ๋žŒ์ด ํ•ด๋‹น ๊ฒŒ์‹œ๊ธ€์„ ๋ณด๋ฉด ์ดํ•ด๋ฅผ ๋ชป ํ•˜์‹ค ๊ฒƒ ๊ฐ™๋‹ค.
๋‹ค๋ฅธ ๋ธ”๋กœ๊ทธ๋ฅผ ์ฐธ๊ณ ํ•ด ๊ฒŒ์‹œ๊ธ€ ๋‚ด์šฉ์„ ๋‹ค๋ฅธ ๋ถ„๋“ค์ด ๋ดค์„ ๋•Œ ๋„์›€์ด ๋˜๊ณ  ์ดํ•ด ๋˜๋„๋ก ์กฐ๊ธˆ์”ฉ ์ˆ˜์ •ํ•ด ๋ณด์ž.

profile
[ํ”„๋ก ํŠธ ์• ์†ก์ด] ์ž‘์€ ๊นจ๋‹ฌ์Œ๋„ ๊ธฐ๋กํ•˜๊ธฐ

0๊ฐœ์˜ ๋Œ“๊ธ€