깜지 팀엔 디자이너가 있다. 처음에는 프론트엔드 파트(나)에서 디자인을 포함한 화면의 모든 것을 담당했었는데 UX/UI 역량의 한계를 느끼고 디자이너 분을 모시게 되었다.
UX/UI 디자이너가 있어 좋은 점은 개발자가 기능적으로 돌아가게만 만들어두었던 부분들을 좀 더 사용자에게 편의한 방면으로 개선할 수 있다는 것이다.
바로 예시를 들어보자면 이번 스프린트에서 개선한 부분인 문제 제출 페이지이다. 사용자들 중 기존 문제 제출 페이지에서 챌린지 정보를 볼 수 없어 문제를 잘못 제출하는 경우가 생겼다. 문제 제출 화면에서 챌린지 정보를 보여주는 기능을 추가하기로 했고, 하는 김에 전체적으로 문제 제출 화면을 개선하기로 했다.
기존 문제 제출 화면
새로 디자인된 문제 제출 화면
화면 상에서 예외처리도 한층 강화되었고, 사용자가 현재 글자수를 볼 수 있는 등 편의성이 높아졌다.
이미지 DnD 기능에 대해서는 디자이너 님께서 짧게 카톡으로 의견을 물어봐주셨다. 사실 한 번도 구현해보지 않는 부분이라 걱정은 됐지만... 이번 기회에 해보는 게 좋을 것 같아 바로 괜찮다고 말씀드렸다.
기존 이미지 첨부 기능은 버튼 하나로 돼있고 이미지 파일이 첨부됐을 때 파일의 제목이 나타난다. 물론 이미지 업로드에는 전혀 문제가 없지만 이미지를 제대로 업로드 했는지 미리보기로 확인할 수 없다는 점 등의 불편함이 예상된다.
제안해주신 이미지 첨부 기능을 살펴보면 다음과 같다.
이미지를 Drag and drop 또는 버튼 클릭으로 첨부할 수 있다. 드래그 중일 때는 점선과 버튼 디자인이 변경된다.
첨부된 이미지는 미리보기 할 수 있고, 삭제할 수 있다.
자...
그럼 이제 본격적으로 구현해볼까!!!!
드래그한 이미지 파일을 받을 컴포넌트(드롭 대상)를 생성하고 Drag event listener를 등록한다.
onDragEnter
onDragOver
onDragLeave
onDrop
e.preventDefault();
e.stopPropagation();
위 코드를 모든 이벤트 핸들러에 작성해주어야 한다. 브라우저에 기본적으로 등록된 이벤트 동작이 있기 때문이다. 지금 사용 중인 벨로그 에디터를 예로 들자면, 글 작성 시 에디터 위로 이미지를 드래그 앤 드롭 하면 새 탭에서 이미지가 열린다.
깜지는 정책 상 한 문제 당 하나의 이미지만 첨부할 수 있으므로 contentImage
라는 File 형식의 state로 첨부된 이미지를 관리해준다.
구현 내용 1번을 보면 dragging 중일 때 디자인을 변경해주어야 하므로 isDragging
state도 선언한다. 이 부분은 Styled Component의 props 기능을 통해 state에 따라 디자인을 변경해준다.
interface Props {
contentImageState: {
contentImage: File | null;
setContentImage: Function;
};
}
function ImageInputBlock({ contentImageState }: Props) {
const [isDragging, setIsDragging] = useState(false);
// 부모 컴포넌트에서 내려준 contentImage state
const { contentImage, setContentImage } = contentImageState;
const onDragEnter = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
};
const onDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
};
const onDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
if (e.dataTransfer.files) {
setIsDragging(true);
}
};
const onDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setContentImage(e.dataTransfer.files[0]);
setIsDragging(false);
};
return (
<DndBox
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
onDragOver={onDragOver}
onDrop={onDrop}
// styled components props
isDragging={isDragging}
>
</DndBox>
);
}
드래그 앤 드롭 기능만 있으면 될게 아니라, 이미지 가져오기 버튼을 통해서도 이미지를 첨부할 수 있어야 한다. input 요소와 label 요소를 id-htmlFor로 연결해주면 label을 눌렀을 때 input을 누른 것처럼 동작한다. 디자인을 위해 input은 화면에서 보이지 않도록 display: none
을 적용한다.
input에서 Change Event가 발생했을 때 Drop Event 상황과 같이 contentImage state를 업데이트한다.
const onContentImageChange = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setContentImage(e.target.files[0]);
}
};
return (
<input
type="file"
accept=".png,.jpg,.jpeg"
id="input-file"
style={{ display: "none" }}
aria-hidden
onChange={onContentImageChange}
/>
<DndBox
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
onDragOver={onDragOver}
onDrop={onDrop}
isDragging={isDragging}
>
<label htmlFor="input-file" role="button">
이미지 가져오기
</label>
</DndBox>
)
이제 구현 내용 2번의 이미지 미리보기 기능을 만들어보자.
자바스크립트 FileReader를 통해 contentImage를 Url로 변환하면 img 태그로 렌더링할 수 있다. readImage 함수는 onDrop과 onContentImageChange에서 호출하며, 인자로는 File 형식의 이미지를 받는다.
이미지 클릭 시 원본 이미지 모달을 띄우는 기능은 간단하므로 생략. 이미지 삭제 기능은 contentImage, contentImageUrl state만 null로 바꿔주면 된다.
const [contentImageUrl, setContentImageUrl] = useState<string | null>(null);
const readImage = (image: File) => {
const reader = new FileReader();
reader.onload = function (e) {
setContentImageUrl(String(e.target?.result));
};
reader.readAsDataURL(image);
};
const onDrop = (e: React.DragEvent<HTMLDivElement>) => {
// ...
readImage(e.dataTransfer.files[0]);
};
const onContentImageChange = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setContentImage(e.target.files[0]);
readImage(e.target.files[0]);
}
};
return (
<ImageBox>
{contentImageUrl && (
<img
alt="문제 이미지 미리보기"
src={contentImageUrl}
onClick={() => onModalStateChange({ state: true })}
/>
)}
</ImageBox>
)
짠~ 막상 해보니 크게 어려운 기능은 아니었다. 역시 해보기 전까진 모르는 것...
그래도 열심히 개발한 나. 멋지다.
좋은 글 너무 잘 봤어요^^