📌 작성자 개발 환경
OS: Mac OS
Tool: Visual Studio Code

5 분 소요

til

업로드 버튼

커스텀 페이지에서 그림판에 로컬 이미지를 삽입하기 위한 업로드 버튼 구현 til

업로드 버튼 레이아웃


//UploadButton.tsx
import styled from 'styled-components';
import React from 'react';
import upload from '../../../assets/images/img_modal/upload.png';

// 스타일드 컴포넌트로 스타일을 정의
const InputStyled = styled.input.attrs({
  type: 'file',
  accept: 'image/*',
})`
  // 스타일 정의
  // ...
`;

// 업로드 버튼 컴포넌트 정의
type UploadButtonProps = {
  onUpload: (imageUrl: string) => void;
};

const UploadButton: React.FC<UploadButtonProps> = ({ onUpload }) => {
  // 업로드된 이미지 처리 함수
  const handleUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
    // input 요소에서 업로드된 파일을 가져옵니다.
    const file = event.target.files?.[0];
    if (file) {
      const reader = new FileReader();
      reader.onload = (e) => {
        // 이미지 파일을 로드한 후 처리
        const image = new Image();
        image.onload = () => {
          // 이미지 크기 조정을 위한 작업 시작
          const canvas = document.createElement('canvas');
          const ctx = canvas.getContext('2d');

          const aspectRatio = image.width / image.height;
          const maxWidth = 200;
          const maxHeight = 200;

          let width = maxWidth;
          let height = maxHeight;

          // 이미지의 종횡비에 따라 크기 조정
          if (maxWidth / maxHeight > aspectRatio) {
            height = maxWidth / aspectRatio;
          } else {
            width = maxHeight * aspectRatio;
          }

          canvas.width = width;
          canvas.height = height;
          ctx?.drawImage(image, 0, 0, width, height);

          // 조정된 이미지를 데이터 URL로 변환
          const imageUrl = canvas.toDataURL('image/png');

          // 변환된 이미지 URL을 부모 컴포넌트로 전달
          onUpload(imageUrl);
        };

        // 이미지 소스를 로드
        image.src = e.target?.result as string;
      };
      // 파일을 데이터 URL로 읽기
      reader.readAsDataURL(file);
    }
  };

  // 업로드 버튼을 렌더링
  return <InputStyled type="file" onChange={handleUpload} />;
};

export default UploadButton;

input 요소에 type 속성을 'file'로 설정하고, 이미지 파일만 업로드할 수 있도록 accept 속성을 'image/*'로 설정

const InputStyled = styled.input.attrs({
  type: 'file',
  accept: 'image/*',
})`

til

handleUpload 함수


type UploadButtonProps = {
  onUpload: (imageUrl: string) => void,
};

const UploadButton: React.FC<UploadButtonProps> = ({ onUpload }) => {
  const handleUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
    // ...
  };

  return <InputStyled type="file" onChange={handleUpload} />;
};

export default UploadButton;
  1. onUpload 함수를 프로퍼티로 받음
  2. handleUpload 함수는 파일 업로드 시 실행
  3. 파일을 읽어와 이미지를 크기 조정하고 데이터 URL로 변환한 후, onUpload 함수를 호출하여 변환된 이미지 URL을 부모 컴포넌트로 전달
  4. onChange 이벤트가 발생하면 handleUpload 함수가 실행되도록 한다.
  5. 반환되는 JSX는 스타일드 컴포넌트로 정의한 InputStyled 컴포넌트로, 실제 업로드 버튼을 렌더링

이미지 업로드 및 처리


const handleUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
  const file = event.target.files?.[0];
  if (file) {
    const reader = new FileReader();
    reader.onload = (e) => {
      const image = new Image();
      image.onload = () => {
        // ...
      };

      image.src = e.target?.result as string;
    };
    reader.readAsDataURL(file);
  }
};

  1. event.target.files를 통해 업로드된 파일에 접근하고, 첫 번째 파일을 가져온다.
  2. 파일이 존재할 경우 ` FileReader를 생성하고, 해당 파일을 데이터 URL`로 읽어온다.
  3. reader.onload 콜백 함수는 데이터 URL을 이미지의 src에 할당하여 이미지를 로드한다.
  4. 이미지 로드가 완료되면 이미지의 크기를 조정하고 데이터 URL로 변환하여 onUpload 함수를 호출하여 부모 컴포넌트로 전달한다.

이미지 크기 조정


const aspectRatio = image.width / image.height;
const maxWidth = 200;
const maxHeight = 200;

let width = maxWidth;
let height = maxHeight;

if (maxWidth / maxHeight > aspectRatio) {
  height = maxWidth / aspectRatio;
} else {
  width = maxHeight * aspectRatio;
}
  1. 이미지의 종횡비최대 크기를 기준으로 이미지의 크기를 조정
  2. aspectRatio 변수에 이미지의 가로와 세로 비율을 저장
  3. maxWidthmaxHeight에 최대 가로 및 세로 크기를 지정
  4. 종횡비에 따라 이미지의 크기를 조정하여 width와 height에 할당

이미지 데이터 URL 변환


canvas.width = width;
canvas.height = height;
ctx?.drawImage(image, 0, 0, width, height);

const imageUrl = canvas.toDataURL("image/png");

onUpload(imageUrl);
  1. 조정된 이미지를 canvas 요소에 그린다.
  2. canvas의 크기를 width와 height로 설정하고, ctx?.drawImage 메서드로 이미지를 그린다.
  3. canvas.toDataURL 메서드를 사용하여 조정된 이미지를 데이터 URL로 변환한다.
  4. 변환된 이미지 URL을 onUpload 함수를 통해 부모 컴포넌트로 전달한다.

앞으로가기 뒤로가기 버튼

버튼 레이아웃

Undo/Redo의 레이아웃은 동일하게 제작하였다.

import styled from "styled-components";
import undo from "../../../assets/images/img_modal/undo.png";

const ButtonStyled = styled.div`
  position: relative;
  z-index: 20;
  width: 30px;
  height: 30px;
  margin-left: 20px;
  border: none;
  cursor: grab;

  background-image: url("${undo}");

  background-repeat: no-repeat;
  background-size: cover;
  background-position: center;

  &::before {
    content: "";
    position: absolute;
    top: -6px;
    left: -6px;
    right: -6px;
    bottom: -6px;
    border-radius: 50%;
    background-color: rgba(0, 0, 0, 0.1);
    transform: scale(0);
    transition: transform 0.2s ease-in-out;
  }

  &:hover::before {
    transform: scale(1);
  }

  &:active {
    filter: brightness(1.2);
  }
`;

type UndoButtonProps = {
  onUndo: () => void,
};

function UndoButton({ onUndo }: UndoButtonProps) {
  return <ButtonStyled onClick={onUndo}></ButtonStyled>;
}

export default UndoButton;

이미지 Undo/Redo 구현

Undo


handleUndoButtonClick 함수는 Undo 버튼이 클릭되었을 때 실행되는 이벤트 처리 로직

const handleUndoButtonClick = () => {
  // 이미지 모드에서 Undo
  if (undoMode === "image" && images.length > 0) {
    const updatedImages = [...images];
    const lastImage = updatedImages.pop();

    if (lastImage) {
      setImages(updatedImages);
      updateImages(updatedImages);
      setRedoImages([lastImage, ...redoImages]);
      setRedoMode("image");
    }
  }
  // 그림 그리기 모드에서 Undo
  else if (undoMode === "drawing" && penDrawings.length > 0) {
    const updatedPenDrawings = [...penDrawings];
    const lastPenDrawing = updatedPenDrawings.pop();

    if (lastPenDrawing) {
      setPenDrawings(updatedPenDrawings);
      setRedoPenDrawings([lastPenDrawing, ...redoPenDrawings]);
      setRedoMode("drawing");
    }
  }

  setDraggedImage(null);
  setDraggedImageIndex(-1);
};

먼저, undoMode 상태에 따라 이미지 모드와 그림 그리기 모드를 구분하여 처리한다.

이미지 모드일 때

  • images 배열의 복사본 updatedImages를 만들고, 마지막 이미지를 제거
  • 제거한 마지막 이미지를 lastImage로 가져와, lastImage를 Undo 이미지 배열에 추가하고, 해당 배열을 redoImages로 설정
  • setImages를 통해 이미지 배열을 업데이트하고, updateImages 함수를 호출하여 상위 컴포넌트에도 업데이트를 전파
  • setRedoMode를 호출하여 Redo 모드를 'image'로 설정

그림 그리기 모드일 때

  • penDrawings 배열의 복사본 updatedPenDrawings를 만들고, 마지막 그림 그리기 정보를 제거
  • 제거한 마지막 그림 그리기 정보를 lastPenDrawing으로 가져와, lastPenDrawing을 Undo 그림 그리기 정보 배열에 추가하고, 해당 배열을 redoPenDrawings로 설정
  • setPenDrawings를 통해 그림 그리기 정보 배열을 업데이트하고, setRedoPenDrawings를 호출하여 Redo 그림 그리기 정보 배열을 업데이트
  • setRedoMode를 호출하여 Redo 모드를 ‘drawing’으로 설정

Redo


const handleRedoButtonClick = () => {
  if (redoMode === "image" && redoImages.length > 0) {
    const updatedRedoImages = [...redoImages];
    const redoImage = updatedRedoImages.shift();

    if (redoImage) {
      // Redo 이미지를 이미지 배열에 추가하고, Redo 이미지 배열 업데이트
      setImages([redoImage, ...images]);
      updateImages([redoImage, ...images]);
      setRedoImages(updatedRedoImages);
    }
  } else if (redoMode === "drawing" && redoPenDrawings.length > 0) {
    const updatedRedoPenDrawings = [...redoPenDrawings];
    const redoPenDrawing = updatedRedoPenDrawings.shift();

    if (redoPenDrawing) {
      // Redo 그림 그리기 정보를 그림 그리기 배열에 추가하고, Redo 그림 그리기 배열 업데이트
      setPenDrawings([redoPenDrawing, ...penDrawings]);
      setRedoPenDrawings(updatedRedoPenDrawings);
    }
  }

  // Redo 이미지와 그림 그리기 배열이 모두 비어있으면 Redo 모드를 'none'으로 설정
  if (redoImages.length === 0 && redoPenDrawings.length === 0) {
    setRedoMode("none");
  }
};

Undo와 유사하게 redoMode 상태에 따라 이미지 모드와 그림 그리기 모드를 구분하여 처리

이미지 모드일 때

  • redoImages 배열의 복사본 updatedRedoImages를 만들고, 첫 번째 Redo 이미지를 가져와 shift 메서드로 배열에서 제거
  • 가져온 첫 번째 Redo 이미지를 현재 이미지 배열의 맨 앞에 추가하고, updateImages 함수를 호출하여 상위 컴포넌트에 업데이트를 전파
  • setRedoImages를 호출하여 Redo 이미지 배열을 업데이트

그림 그리기 모드일 때

  • redoPenDrawings 배열의 복사본 updatedRedoPenDrawings를 만들고, 첫 번째 Redo 그림 그리기 정보를 가져와 shift 메서드로 배열에서 제거
  • 가져온 첫 번째 Redo 그림 그리기 정보를 현재 그림 그리기 정보 배열의 맨 앞에 추가하고, setRedoPenDrawings를 호출하여 Redo 그림 그리기 정보 배열을 업데이트
  • 마지막으로, Redo 이미지 배열과 Redo 그림 그리기 정보 배열이 모두 비어있으면 Redo 모드를 'none'으로 설정하여 더 이상 Redo가 불가능한 상태임을 나타냄

결과(영상)

til

댓글남기기