[React] 게시글 이미지 업로드 구현 (feat. 코드 개선하기)

·2023년 4월 24일
0

React

목록 보기
2/8
post-thumbnail

SNS 게시글 업로드시 텍스트와 이미지 한 장을 업로드할 수 있는 기능을 구현 중이었다.

이미지 미리보기를 띄울 때 이미지 업로드 준비도 하도록 한번에 구현하고 있었다.
(api 분리를 하지 않은 상태)

개선 전 코드

  const [isActive, setIsActive] = useState(false);
  const [text, setText] = useState('');
  const [imgFile, setImgFile] = useState('');
  const [prevImgFile, setPrevImgFile] = useState('');
  const [previewImgUrl, setPreviewImgUrl] = useState('');

// 
useEffect(() => {
    const getPostData = async () => {
      try {
        const res = await API.get(`/post/${id}`, {
          headers: {
            Authorization: `Bearer ${auth}`,
            'Content-type': 'application/json',
          },
        });
        const { post } = res.data;
        if (post.content) {
          setText(post.content);
        }
        if (post.image) {
          setPrevImgFile(post.image);
          setPreviewImgUrl(post.image);
        }
      } catch (err) {
        if (err.response) {
          console.log(err.response.data);
          console.log(err.response.status);
          console.log(err.response.headers);
        } else {
          console.log(`Error: ${err.message}`);
        }
      }
    };
    getPostData();
  }, []);

// 텍스트나 이미지가 있을 때 업로드 버튼 활성화
useEffect(() => {
    if (text || previewImgUrl) {
      setIsActive(true);
    } else {
      setIsActive(false);
    }
  }, [text, previewImgUrl]);

  const textChangeHandler = (e) => {
    setText(e.target.value);
  };

// 이미지 업로드 기능
// api 명세에서 이미지를 피드에서 띄우기 위해 `https://... url.../${res.data.filename}` 로 변환해줘야 하는 과정이 필요했다.
// 이때 이미지 미리보기와 업로드 준비를 이미지 업로드 핸들러 하나에서 구현하고 있었다.
  const imgUploadHandler = async (file) => {
    if (!file) {
      return prevImgFile;
    }
    const formData = new FormData();
    formData.append('image', file);
    try {
      const res = await API.post('/image/uploadfile', formData, {
        headers: {
          'Content-Type': 'multipart/form-data',
        },
      });
      const feedImgUrl = `https://... url.../${res.data.filename}`;
      return feedImgUrl;
    } catch (err) {
      if (err.response) {
        console.log(err.response.data);
        console.log(err.response.status);
        console.log(err.response.headers);
      } else {
        console.log(`Error: ${err.message}`);
      }
      return null;
    }
  };

// 미리보기 이미지 핸들러
  const previewImgHandler = (e) => {
    const correctForm = /(.*?)\.(jpg|gif|png|jpeg|bmp|tif|heic|)$/;
    const file = e.target.files[0];
    const fileReader = new FileReader();
    if (previewImgUrl.length > 0) {
      alert('1개의 이미지 파일을 업로드 하세요.');
      return;
    }
    if (file.size > 1024 * 1024 * 10) {
      alert('10MB 이상의 이미지는 업로드 할 수 없습니다.');
      return;
    }
    if (!file.name.match(correctForm)) {
      alert(
        '이미지 파일만 업로드가 가능합니다. (*.jpg, *.gif, *.png, *.jpeg, *.bmp, *.tif, *.heic)'
      );
    } else {
      fileReader.readAsDataURL(file);
      setImgFile(file);
    }
    fileReader.onload = () => {
      setPreviewImgUrl(fileReader.result);
      e.target.value = null;
    };
  };

// 이미지 삭제
  const deleteImgHandler = (e) => {
    e.preventDefault();
    setPreviewImgUrl('');
    setImgFile('');
    e.target.value = null;
  };

return (
    <PageWrapStyle>
      <Header
        id={id}
        size="ms"
        variant={isActive ? '' : 'disabled'}
        onClick={editUploadHandler}
        disabled={!(text || previewImgUrl)}
      >
        업로드
      </Header>
      <ConWrapStyle>
        <PostFormStyle>
          <TextAreaStyle
            type="text"
            value={text}
            placeholder="게시글 입력하기"
            onChange={textChangeHandler}
          />
          {previewImgUrl && (
            <ImgWrapStyle>
              <PreviewImgWrapStyle>
                <PreviewImg src={previewImgUrl} alt="이미지 미리보기" />
                <DeleteImgBtn type="button" onClick={deleteImgHandler} />
              </PreviewImgWrapStyle>
            </ImgWrapStyle>
          )}
          <BtnWrapStyle>
            <UploadFileBtn onChange={previewImgHandler} />
          </BtnWrapStyle>
        </PostFormStyle>
      </ConWrapStyle>
    </PageWrapStyle>
  );

이미지를 첨부할 때마다 서버에 업로드를 하게 구현해놓았다. 이미지를 삭제하고 수정할때마다 요청을 보내는 것도 잘못된 것은 아니지만 해당 이미지가 실제로 게시글에 사용될지 아닐지 모르는 상황에서 업로드하는 것은 번거로운 작업이라고 느꺘다.

이미지 업로드시마다 API 요청을 보내기보단 따로 값으로 이미지들을 받아놓고 업로드 혹은 수정 버튼 클릭시 -> 준비된 이미지 업로드 + 결과 값을 받아서 -> 게시글 데이터와 함께 게시글 업로드 식으로 가는 것이 좋다고 느꼈다.

  • 이때 유지 보수와 코드 중복 최소화를 위해 서버와 통신하여 호출하는 모든 것들을 API 파일에 분리하여 관리하였다. (복잡성을 낮추어 하나의 폴더만 보고도 API를 빠르게 파악할 수 있도록 하기)
  const [isActive, setIsActive] = useState(false);
  const [text, setText] = useState('');
  const [imgFile, setImgFile] = useState('');
  const [previewImgUrl, setPreviewImgUrl] = useState('');

  useEffect(() => {
    if (text || previewImgUrl) {
      setIsActive(true);
    } else {
      setIsActive(false);
    }
  }, [text, previewImgUrl]);

  const textChangeHandler = (e) => {
    setText(e.target.value);
  };

// 이미지 미리보기
  const previewImgHandler = (e) => {
    const correctForm = /(.*?)\.(jpg|gif|png|jpeg|bmp|tif|heic|)$/;
    const file = e.target.files[0];
    const fileReader = new FileReader();
    if (previewImgUrl.length > 0) {
      alert('1개의 이미지 파일을 업로드 하세요.');
      return;
    }
    if (file.size > 1024 * 1024 * 10) {
      alert('10MB 이상의 이미지는 업로드 할 수 없습니다.');
      return;
    }
    if (!file.name.match(correctForm)) {
      alert(
        '이미지 파일만 업로드가 가능합니다. (*.jpg, *.gif, *.png, *.jpeg, *.bmp, *.tif, *.heic)'
      );
    } else {
      fileReader.readAsDataURL(file);
      setImgFile(file);
    }
    fileReader.onload = () => {
      setPreviewImgUrl((preview) => [...preview, fileReader.result]);
      e.target.value = null;
    };
  };

// 이미지 삭제
  const deleteImgHandler = (e) => {
    e.preventDefault();
    setPreviewImgUrl('');
    setImgFile('');
    e.target.value = null;
  };

// 이미지 업로드 핸들러를 미리보기와 명확하게 분리하였다.
// 이미지를 업로드하는 버튼을 눌렀을 때, `${baseUrl}/${res[0].filename}`로 반환하도록 했다.
  const imgUploadHandler = async (file) => {
    const formData = new FormData();
    formData.append('image', file);
    if (file) {
      const res = await postImage(formData);
      const feedImgUrl = `${baseUrl}/${res[0].filename}`;
      return feedImgUrl;
    } else {
      return null;
    }
  };

// 포스트 업로드
  const postUploadHandler = async () => {
    const image = await imgUploadHandler(imgFile);
    const postData = {
      post: {
        content: text,
        image,
      },
    };
    const res = await createPost(postData);
    navigate(`/my_profile/${accountName}`);
  };

    return (
      <>
        <PageWrapStyle>
          <Header
            size="ms"
            variant={isActive ? '' : 'disabled'}
            onClick={postUploadHandler}
            disabled={!(text || previewImgUrl)}
          >
            업로드
          </Header>
          <ConWrapStyle>
            <H2IR>게시글 작성창</H2IR>
            <PostFormStyle>
              <TextAreaStyle
                type="text"
                placeholder="게시글 입력하기"
                onChange={textChangeHandler}
              />
              {previewImgUrl && (
                <ImgWrapStyle>
                  <PreviewImgWrapStyle>
                    <PreviewImg src={previewImgUrl} alt="이미지 미리보기" />
                    <DeleteImgBtn type="button" onClick={deleteImgHandler} />
                  </PreviewImgWrapStyle>
                </ImgWrapStyle>
              )}
              <BtnWrapStyle>
                <UploadFileBtn onChange={previewImgHandler} />
              </BtnWrapStyle>
            </PostFormStyle>
          </ConWrapStyle>
        </PageWrapStyle>
      </>
    );

그리하여 개선된 코드!
훨씬 더 가독성이 좋아졌다.
코드 길이가 길어지는 것보다도 핸들러 하나에는 한 기능만 구현할 것!

게시글 업로드 하나에 많은 경우의 수를 고려해야했다. (글만 올렸을 때, 이미지만 올렸을 때, 미리보기 이미지를 삭제했다가 다른 이미지를 올렸을 때, 미리보기 이미지를 삭제했다가 같은 이미지를 올렸을 때...)
많은 테스트를 거치며 고민해야했지만 무척 뿌듯한 작업이었다.

profile
주니어 프론트엔드 웹 개발자 🐛

0개의 댓글