패스트캠퍼스 데브캠프 80~81일차 [React, useImperativeHandle]

Su Min·2024년 9월 13일
0
post-thumbnail

useImperativeHandle Hook을 사용하여 자식 컴포넌트에서 부모 컴포넌트로 props 전달하기

토이3 프로젝트 중 플레이리스트를 추가하고 수정 할 수 있는 관리페이지를 담당하면서 부모컴포넌트인 managePlaylist.tsx가 있었고, 하위로 영상을 추가 할 양식 컴포넌트 AddPlaylist.tsx와 추가된 영상 아이템들을 목록으로 띄울 PlaylistChart.tsx가 있었다.
자식 컴포넌트인 AddPlaylist.tsx에서 입력된 데이터와 스토어에 저장된 아이템들의 목록을 최종적으로 부모 컴포넌트인 managePlaylist.tsx에서 API를 이용해 데이터를 저장하는 방식이다. 그렇기때문에 useRefuseImperativeHandle 훅을 사용하여 자식 컴포넌트의 데이터를 부모 컴포넌트에서 받아올 수 있도록 하였다.

🔗 기본 사용법

부모컴포넌트

...
const data = useRef(null)

// getData.current?.getData()?.title 자식컴포넌트의 데이터 가져오기

return (
  <ChildrenFC ref={data} />  
)
...

자식컴포넌트

// ref prop을 전달하기 위해 forwardRef를 사용
const ChildrenFC = forwardRef(({}, ref) => {
  const title = useRef<HTMLInputElement>(null)
  
  useImperativeHandle(ref, () => ({
  	getData,
  }))
  
  // useCallback을 사용하여 부모가 최신 상태의 value를 받을 수 있도록 한다.
  const getData = useCallback(() => { 
    const data: {
      title: ''
    }
    data.title = title.current?.value || ''
    return data
  }, [title])
  
  return (
    <>
      <input ref={title} />
    </>
  )
})

🔗 managePlaylist.tsx

부모컴포넌트는 온전히 자식컴포넌트와 스토어의 데이터만으로 최종적인 유효성검사와 훅을 사용하여 데이터베이스에 저장해주는 역할만 있으며 자식컴포넌트에서 받은 데이터는 playlistDataToAdd이다.

const ManagePlaylist = () => {
  const userData = useUserStore((state) => state.userInformation);
  const addedPlaylist = useYoutubeDataStore((state) => state.youTubelistData);
  const clearYoutubelistData = useYoutubeDataStore((state) => state.clearYoutubelistData);
  const playlistDataToAdd = useRef<{ getPlaylistData: () => PlaylistDataStore }>(null);
  const { playlistId } = useParams() as { playlistId: string };
  const { mutate } = useNewPlaylist(playlistId);
  const navigate = useNavigate();
.
.
.
// 자식컴포넌트 데이터의 최종 유효성검사
  const handleValidation = (type: string) => {
    if (playlistDataToAdd.current?.getPlaylistData()?.title === '') {
      toast.error('플레이리스트 제목을 입력해주세요.');
    } else if (playlistDataToAdd.current?.getPlaylistData()?.content === '') {
      toast.error('플레이리스트 설명을 입력해주세요.');
    } else if (addedPlaylist.length === 0) {
      toast.error('플레이리스트를 추가해주세요.');
    } else {
      fetchCreatePlaylistData(type);
    }
  };

// 데이터베이스에 저장하기 위한 API요청
  const fetchCreatePlaylistData = (type: string) => {
    const newPlyData = playlistDataToAdd.current?.getPlaylistData();
    if (newPlyData) {
      const playlistData = {
        userId: userData.userId,
        title: newPlyData.title,
        content: newPlyData.content,
        disclosureStatus: newPlyData.disclosureStatus,
        tags: newPlyData.tags,
        link: addedPlaylist.map((item) => item.link?.[0]),
        imgUrl: addedPlaylist.map((item) => item.imgUrl?.[0]),
      };
      try {
        mutate({ playlistData, type, playlistId });
        if (type === '추가') {
          toast.success('플레이리스트가 생성되었습니다.');
        } else if (type === '수정') {
          toast.success('플레이리스트가 수정되었습니다.');
        }
        setTimeout(() => {
          clearYoutubelistData();
          navigate(`/playlist/${userData.userId}`);
        }, 1000);
      } catch (error) {
        toast.error('플레이리스트 업데이트 중 오류가 발생하였습니다. 다시 시도해주세요.');
        console.error(error);
      }
    }
  };

  return (
    <div css={{ width: '100%', margin: '5px 15px 0 0' }}>
      <div css={{ display: 'flex' }}>
        <AddPlaylist ref={playlistDataToAdd} userPlyData={userPlyData} />
        <div css={{ width: 'calc(100% - 450px)', position: 'relative' }}>
          <PlaylistChart />
          <div css={btnArea}>
            <Button size="md" onClick={() => history.back()}>
              취소
            </Button>
            <Button
              size="md"
              background={true}
              onClick={() => {
                if (playlistId) {
                  handleValidation('수정');
                } else {
                  handleValidation('추가');
                }
              }}
            >
              완료
            </Button>
          </div>
        </div>
        <ToastContainer
          position="bottom-center"
          limit={2}
          closeButton={false}
          autoClose={2000}
          hideProgressBar
        />
      </div>
    </div>
  );
};

export default ManagePlaylist;

🔗 Addplaylist.tsx

useCallback을 사용하여 value들이 모두 최신 상태로 유지되게끔 하여 부모 컴포넌트에서 데이터를 정상적으로 받을 수 있게 된다.

interface AddPlaylistProps {
  userPlyData: IPlaylist | null | undefined;
}
interface AddPlaylistRef {
  getPlaylistData: () => PlaylistDataStore;
}

const AddPlaylist = forwardRef<AddPlaylistRef, AddPlaylistProps>(({ userPlyData }, ref) => {
  const [tags, setTags] = useState<string[]>([]);
  const [content, setContent] = useState('');
  const [disclosureStatus, setDisclosureStatus] = useState(true);
  const title = useRef<HTMLInputElement>(null);

  useImperativeHandle(ref, () => ({
    getPlaylistData,
  }));

  const getPlaylistData = useCallback(() => {
    const playlistData: PlaylistDataStore = {
      title: '',
      content: '',
      disclosureStatus,
      tags: [],
    };
    playlistData.title = title.current?.value || '';
    playlistData.content = content;
    playlistData.disclosureStatus = disclosureStatus;
    playlistData.tags = tags;
    return playlistData;
  }, [title, content, tags, disclosureStatus]);
.
.
.
  return (
    ...
          <input type="text" ref={title} />
          <textarea value={content} onChange={(e) => setContent(e.target.value)}></textarea>
          <input
            type="text"
            ref={url}
            onKeyDown={(e) => handleKeyDown(e, 'url')}
            onCompositionStart={() => setIsComposing(true)}
            onCompositionEnd={() => setIsComposing(false)}
          />
          <input
            type="text"
            ref={tagValue}
            onKeyDown={(e) => handleKeyDown(e, 'tag')}
            onCompositionStart={() => setIsComposing(true)}
            onCompositionEnd={() => setIsComposing(false)}
          />
    ...
  );
});

export default AddPlaylist;
profile
성장하는 과정에서 성취감을 통해 희열을 느낍니다⚡️

0개의 댓글

관련 채용 정보