const handleClick = () => {
fileInputRef.current?.click();
};
이거 구현하는 법?
굵고 짧게 가겠다.
핵심 부분 간단 설명
1. 이미지 선택흐름:
- 사용자가 영역 클릭 -> 파일 선택창 열림 -> 이미지를 Base64 문자열로 변환 -> 화면에 미리 보기 표시
2. 이미지 삭제 흐름:
- 삭제 버튼 클릭 -> 이미지 데이터 초기화 -> "파일 선택" 화면으로 돌아감
컴포넌트 선언
export const ImageUpload = () => {
// 여기에 코드가 들어갑니다
};
상태와 Ref 설정
const [preview, setPreview] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
- preview: 이미지 미리보기 데이터를 저장하는 상태 변수
- fileInputRef: 숨겨진 파일 입력 요소에 접근하기 위한 참조
파일 변경 처리 함수
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file && file.type.startsWith("image/")) {
const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result === "string") {
setPreview(reader.result);
}
};
reader.readAsDataURL(file);
}
};
- e.target.files?.[0]: 선택된 첫 번째 파일을 가져온다?. 은 files가 null일 수도 있기에 안전하게 접근하기 위해서 옵셔널로 작성했다.
- fifle.type.startsWith("image/"): 파일이 이미지인지 확인한다
- FileReader: 파일을 문자열로 읽기 위한 도구
- reader.onload: 파일 읽기가 완료되면 실행될 함수
- reader.readAsDataURL(file): 파일을 Base64 인코딩된 데이터 URL로 읽기 시작한다
- setPreview(reader.result): 읽은 이미지 데이터를 상태에 preview 상태에 저장한다
클릭 처리 함수
const handleClick = () => {
fileInputRef.current?.click();
};
- fileInputRef.current?.click(): 숨겨진 파일 입력 요소를 프로그래밍적으로 클릭한다
삭제 처리 함수
const handleDelete = (e: React.MouseEvent) => {
e.stopPropagation();
setPreview(null);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
- e.stopPropagation(): 이벤트가 상위 요소로 전파되는 것을 막는다(부모의 handleClick이 실행되지 않도록)
- setPreview(null): 미리보기 이미지 제거
- fileInputRef.current.value = "": 파일 입력 요소를 초기화하여 같은 파일을 다시 선택할 수 있게 한다
렌더링 부분
return (
<ImageWrapper onClick={handleClick}>
{preview ? (
<PreviewContainer>
<PreviewImage src={preview} alt="선택된 이미지" />
<DeleteButton onClick={handleDelete}>
<DeleteIcon />
</DeleteButton>
</PreviewContainer>
) : (
<PlaceholderContent>
<IconPlus />
<UploadText>파일 선택</UploadText>
</PlaceholderContent>
)}
<HiddenInput
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleFileChange}
/>
</ImageWrapper>
);
전체 동작 흐름
1. 초기 상태: 처음에는 preview가 null이라 + 아이콘과 "파일 선택" 텍스트가 보임
2. 이미지 선택:
- 사용자가 영역을 클릭하면 -> handleClick 실행 -> 숨겨진 파일 입력 클릭 -> 파일 선택 창 열림
- 이미지 선택하면 -> handleFileChange 실행 -> 파일 읽기 -> preview 상태 업데이트
- preview 상태가 변경되면 -> 컴포넌트 리렌더링 -> 이미지와 삭제 버튼 표시
3. 이미지 삭제:
- 삭제 버튼 클릭하면 -> handleDelete 실행 -> preview를 null로 설정, 파일 입력 초기화
- preview가 null로 변경되면 -> 컴포넌트 리렌더링 -> 다시 + 아이콘과 "파일 선택" 텍스트 표시
전체코드
import React, { useState, useRef } from "react";
import styled from "@emotion/styled";
export const ImageUpload = () => {
const [preview, setPreview] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file && file.type.startsWith("image/")) {
const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result === "string") {
setPreview(reader.result);
}
};
reader.readAsDataURL(file);
}
};
const handleClick = () => {
fileInputRef.current?.click();
};
const handleDelete = (e: React.MouseEvent) => {
e.stopPropagation();
setPreview(null);
// 파일 입력 초기화
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
return (
<ImageWrapper onClick={handleClick}>
{preview ? (
<PreviewContainer>
<PreviewImage src={preview} alt="선택된 이미지" />
<DeleteButton onClick={handleDelete}>
<DeleteIcon />
</DeleteButton>
</PreviewContainer>
) : (
<PlaceholderContent>
<IconPlus />
<UploadText>파일 선택</UploadText>
</PlaceholderContent>
)}
<HiddenInput
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleFileChange}
/>
</ImageWrapper>
);
};
const ImageWrapper = styled.div`
width: 200px;
height: 200px;
border: 2px dashed #dddddd;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
overflow: hidden;
transition: all 0.2s ease-in-out;
position: relative;
&:hover {
border-color: #4dabf7;
background-color: rgba(77, 171, 247, 0.04);
}
`;
const HiddenInput = styled.input`
display: none;
`;
const PreviewContainer = styled.div`
width: 100%;
height: 100%;
position: relative;
`;
const PreviewImage = styled.img`
width: 100%;
height: 100%;
object-fit: cover;
`;
const DeleteButton = styled.button`
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.8);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: all 0.2s ease;
z-index: 2;
&:hover {
background-color: rgba(255, 255, 255, 1);
transform: scale(1.1);
}
`;
const DeleteIcon = styled.div`
width: 12px;
height: 12px;
position: relative;
&:before,
&:after {
content: "";
position: absolute;
height: 100%;
width: 2px;
background-color: #f03e3e;
top: 0;
left: 50%;
}
&:before {
transform: translateX(-50%) rotate(45deg);
}
&:after {
transform: translateX(-50%) rotate(-45deg);
}
`;
const PlaceholderContent = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #868e96;
`;
const IconPlus = styled.div`
width: 40px;
height: 40px;
position: relative;
margin-bottom: 8px;
&:before,
&:after {
content: "";
position: absolute;
background-color: #868e96;
}
&:before {
width: 2px;
height: 100%;
left: 50%;
transform: translateX(-50%);
}
&:after {
height: 2px;
width: 100%;
top: 50%;
transform: translateY(-50%);
}
`;
const UploadText = styled.p`
margin: 0;
font-size: 14px;
font-weight: 500;
`;'개발 > React' 카테고리의 다른 글
| hydration이 무엇인지 모른다면 이 글을 읽으세요. (0) | 2026.03.11 |
|---|---|
| FSD 아키텍쳐란? (짧고 굵게 알아보기.) (0) | 2026.03.10 |
| useState... 이거는 알고가야죠? (0) | 2025.04.14 |
| 모노레포 프로젝트에서 라이브러리 import 문제 해결하기 (0) | 2025.03.29 |
| 리액트(React) 설치 (1) | 2024.08.26 |