far

[React + tailwindCSS] 게시글 이미지 첨부 및 첨부된 이미지 리스트 만들기 본문

React/기록

[React + tailwindCSS] 게시글 이미지 첨부 및 첨부된 이미지 리스트 만들기

Eater 2023. 1. 21. 19:12

쉽게 할 수 있을거라 생각했는데 많이 고생했던 부분이라 기록으로 남긴다. 구조를 짤 때 백엔드단과의 소통 미스로 textarea안에 이미지를 첨부하기 위한 업로드를 짜야하는줄 알고 빙빙 돌아왔기에 더더욱 오래걸렸다.

 

 

HTML, 기본 Form

const PostForm = () => {

const imageInput = useRef() as React.RefObject<HTMLInputElement>;

  const onClickImageUpload = useCallback(() => {
        if (!imageInput.current) {
            return;
        }
        imageInput.current.click();
    }, [imageInput.current]);

  return (
    <div className="flex pl-0 space-x-1 sm:pl-2">
          <input type="file"
            accept='image/*'
            multiple
            hidden
            ref={imageInput}
            onChange={async (e) => {
              imageStorageFunction(e)
            }}
          />
          <div className='flex'>
            {
              imgl.map((v, i) => {
                return (
                  <div className='flex'>
                    <img src={v} key={i} data-index={i}
                      width="75px" height="75px" ref={imageDeleteInput} />
                  </div>
                )
              })
            }
          </div>
          <button
            className="inline-flex justify-center p-2 text-gray-500 rounded cursor-pointer hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-600"
            onClick={onClickImageUpload}
          >
            <svg
              aria-hidden="true"
              className="w-5 h-5"
              fill="currentColor"
              viewBox="0 0 20 20"
              xmlns="http://www.w3.org/2000/svg">
              <path fillRule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z"
                clipRule="evenodd"></path>
            </svg>
          </button>
    </div>
  )
}

이미지 업로드를 위한 기본적인 틀을 만들었다.

img부분에 src={v}은 백엔드단에서 이미지를 이렇게 받아달라고 하셔서 한 것. 개발 상황에 따라 수정해주면 된다.

 

 

이미지 첨부 후 배열에 넣기

const PostForm = () => {
    const dispatch = useAppDispatch();
    const [imgStorage, setImageStorage] = useState<File[]>([]);
    const [imgl, setImgList] = useState<string[]>([]);
    const imageInput = useRef() as React.RefObject<HTMLInputElement>;
    const imageDeleteInput = useRef<HTMLImageElement | null>(null);

	const imageStorageFunction = useCallback((e: ChangeEvent<HTMLInputElement>) => {
        if (e.target.files?.length) {
            for (let i = 0; i < e.target.files.length; i++) {
                const element = e.target.files[i];
                setImageStorage(file => [...file, element])
                // console.log(element.name);
                let reader = new FileReader();
                reader.readAsDataURL(element)
                reader.onload = (f) => {
                    setImgList(v => [...v, f.target!.result as string])
                }
            }
        }
        e.target.files = null
	}, [imgStorage, imgl])
}

일단 이미지를 전송할 때 필요한 값을 받아주는 imageStorage와 이미지 미리보기를 만들 때 필요한 값을 받아주는 imgl을 만든다.

 

그리고 일단 이미지 데이터를 imageStorage라는 배열에 저장 후 FileReader를 사용해 imgl에서 첨부된 이미지 파일을 읽어올 수 있도록 값을 넣어준다.

 

(FileReader는 Type이 File인 input 태그 또는 API 요청과 같은 인터페이스를 통해 File 또는 Blob 객체를 입력받아 Text로 읽을 수 있게 해주는 객체이다.

FileReader.readAsDataURL 메서드는 컨텐츠를 특정 Blob 이나 File에서 읽어 오는 역할을 하며,

FileReader.onload는 load 이벤트의 핸들로 읽기 동작이 성공적으로 완료되었을 때마다 발생한다.)

https://developer.mozilla.org/ko/docs/Web/API/FileReader

 

이제 첨부된 이미지를 게시글 아래에 미리보기처럼 볼 수 있게 되었다.

 

첨부된 이미지 삭제하기

<div className='flex'>
  {
    imgl.map((v, i) => {
      const deleteImage = () => {
        imgl.splice(i, 1)
        imgStorage.splice(i, 1)
        setImgList(v => [...imgl])
        setImageStorage(imgStorage)
      }
      return (
        <div className='flex'>
          <img src={v} key={i} data-index={i}
            width="75px" height="75px" ref={imageDeleteInput} />
          <div className='absolute bg-gray-200 m-1 px-1 '>
            <button
              type="button"
              onClick={deleteImage}
              className='flex justify-center items-center text-sm font-medium shadow-md text-purple-500 
                          focus:outline-none border border-gray-200 hover:text-indigo-900 focus:z-10 
                          dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 
                          dark:hover:text-white dark:hover:bg-gray-700'>X</button>
          </div>
        </div>
      )
    })
  }
</div>

이미지를 배열에서 제거할 때 imgl.map의 index를 사용하기 위해 함수를 여기다 작성했다. 위에다가 작성했더니 state의 변화가 실시간으로 일어나지 않았기 때문에..

 

imgl의 배열은 읽기 전용 데이터로 들어오기 때문에 setImgList에서 스프래드 연산자를 사용하지 않으면 작동하지 않는다.

 

import { addPost } from '@actions/post';

...

const onSubmit = useCallback(async (e: React.MouseEvent<HTMLButtonElement>) => {
        e.preventDefault();
        const result = new FormData;

        if (imgStorage) {
            for (let i = 0; i < imgStorage.length; i++) {
                result.append("articleImages", imgStorage[i])
                // console.log(imgStorage[i].name);
            }
        }
        dispatch(addPost(result))
}, [])

이제 FormData를 만들어서 미리 redux-toolkit을 사용해 만들어둔 addPost에 dispatch하면 완성이다.

Comments