리액트로 프로필 업로더를 만들어볼 것이다!
구현과정에 대한 설명을 세세하게 적어둔 블로그가 없었기에 많이 헤맸었다.
그래서 나는 최대한 친절하게, 자세히, 하나하나 다 정리해보고자 한다.
1. 프로필 영역을 마크업 한다.
ProfileChange.jsx
import styles from "./ProfileChange.module.css";
import DeleteImg from "../../../assets/my-page/setting/profile-delete.png";
import DefaultImg from "../../../assets/my-page/setting/default-background.png";
const ProfileChange = () => {
return (
<div className={styles["profile-setting-main-profile-change-container"]}>
<p className={styles["profile-setting-main-profile-change-title"]}>프로필 변경</p>
<div className={styles["profile-setting-main-profile-change-img-box"]}>
<img src={DefaultImg} alt="프로필 사진" className={styles["profile-setting-main-profile-change-img"]} />
<label className={styles["profile-setting-main-profile-change-add-img"]} htmlFor="input-file">
<span className={styles["profile-setting-main-profile-change-add-img-icon"]}>블로그 이미지 찾아보기</span>
<input
type="file"
accept="image/*"
id="input-file"
className={styles["profile-setting-main-profile-change-add-img-input"]}
/>
</label>
<button
type="button"
className={styles["profile-setting-main-profile-change-delete"]}
>
</button>
</div>
</div>
);
};
export default ProfileChange;
프로필 사진 영역의 코드를 설명해보겠다.
<div>로 <img>와 <label>을 묶고,
<img> 안에는 회색이미지 ( src={DefaultImg} ) 를 넣는다.
<label> 안에 <span>과 <input>을 넣는다.
<label>의 htmlFor과 <input>의 id는 동일한 이름으로 연결해야함을 유의한다.
<span>안에는 background로 플러스 버튼 이미지를 넣는다.
그리고 '블로그 이미지 찾아보기'를 text-indent - 9999px로 설정하여 화면 상에서 안보이게 한다.
<input>은 type="file"로 설정하고, accept="image/*" 로 하여, 모든 종류의 파일을 허용할 수 있게 하였다.
<input>의 기본 CSS를 display: none 으로 없애고,
파일 찾기 버튼을 없애기 위해
profile-setting-main-profile-change-add-img-input::file-selector-button { display: none; }
으로 설정했다.
2. 파일에서 찾은 이미지를 업로드하는 기능 구현하기
이제 플러스 버튼을 누르면, 파일 찾기가 뜨고, 파일찾기에서 이미지를 선택하면 해당 이미지가 프로필사진으로 들어오게 해보자.
import styles from "./ProfileChange.module.css";
import {useRef, useState} from "react";
import DeleteImg from "../../../assets/my-page/setting/profile-delete.png";
import DefaultImg from "../../../assets/my-page/setting/default-background.png";
const ProfileChange = () => {
const [Image, setImage] = useState(DefaultImg); // (1)번 설명
const [File, setFile] = useState(""); // (2)번 설명
const fileInput = useRef(null); // (3)번 설명
const onChange = (e) => { // (4)번 설명
if (e.target.files[0]) {
setFile(e.target.files[0]);
} else {
//업로드 취소할 시
setImage(DefaultImg);
return;
}
//화면에 프로필 사진 표시
const reader = new FileReader(); // (5)번 설명
reader.onload = () => {
if (reader.readyState === 2) {
setImage(reader.result);
}
};
reader.readAsDataURL(e.target.files[0]);
};
return (
<div className={styles["profile-setting-main-profile-change-container"]}>
<p className={styles["profile-setting-main-profile-change-title"]}>프로필 변경</p>
<div className={styles["profile-setting-main-profile-change-img-box"]}>
<img src={Image} alt="프로필 사진" className={styles["profile-setting-main-profile-change-img"]} />
<label className={styles["profile-setting-main-profile-change-add-img"]} htmlFor="input-file">
<span className={styles["profile-setting-main-profile-change-add-img-icon"]}>블로그 이미지 찾아보기</span>
<input
type="file"
accept="image/*"
id="input-file"
className={styles["profile-setting-main-profile-change-add-img-input"]}
onClick={() => {
fileInput.current.value = null;
fileInput.current.click();
}}
ref={fileInput}
onChange={onChange}
/>
</label>
<button
type="button"
className={styles["profile-setting-main-profile-change-delete"]}
>
</button>
</div>
</div>
);
};
export default ProfileChange;
// (1) 프로필 사진을 표시할 창 부분의 상태관리 ▶ const [Image, setImage] = useState(DefaultImg);
useState를 사용해서 state를 Image로 설정하고, 이것을 프로필 사진 img src에 넣어준다.
▶ img src={Image}
그리고, 초기값은 DefaultImage(회색 배경)로 설정한다.
// (2) 프로필창에 들어갈 파일 상태관리 ▶ const [File, setFile] = useState("");
// (3) Ref 객체 생성하기 ▶ const fileInput = useRef(null);
이 Ref 객체를 선택하고 싶은 DOM에 (여기서는 <input>태그에) ref값으로 설정해준다. ▶ ref={fileInput}
<input>태그(플러스 버튼)에 onClick 이벤트를 넣어서, 사진을 클릭하면 파일 업로더를 띄울 수 있도록한다.
▶ onClick={() => { fileInput.current.value = null; fileInput.current.click() }}
// (4) onChange함수
const onChange = (e) => {
if (e.target.files[0]) {
setFile(e.target.files[0]);
} else {
//업로드 취소할 시
setImage(DefaultImg);
return;
}
<input>에 onChange이벤트로 onChange함수를 달아준다. ▶ onChange={onChange}
파일을 찾아서 업로드를 성공적으로 완료를 하게되면, onChange함수의 if절이 실행돼서, setFile로 인해 상태변화가 일어나게되고, 파일이 input창으로 정상적으로 업로드된다. ▶ setFile(e.target.files[0])
파일 업로드를 취소하면 else절이 실행돼서, 회색 배경 기본 이미지를 설정하도록 해준다.
▶ setImage(DefaultImg)
// (5) FileReader
FileReader을 생성하고 이미지를 정상적으로 불러오면 이미지를 프로필 사진으로 지정한다.
readAsDataURL함수로 받아온 파일을 reader로 불러와준다.
const reader = new FileReader();
reader.onload = () => {
if (reader.readyState === 2) { // 성공적으로 파일을 읽어들였을 때
setImage(reader.result);
}
};
reader.readAsDataURL(e.target.files[0]);
};
첨부된 파일을 웹 화면에서 보기 위해서는, 파일을 File Reader를 사용하여 Data url 형식으로 변환해주어야 한다.
Data Url
Data URIs, 즉 data: 스킴이 접두어로 붙은 URL은 컨텐츠 작성자가 작은 파일을 문서 내에 인라인으로 임베드할 수 있도록 해준다.
FileReader
FileReader 객체는 웹 애플리케이션이 비동기적으로 데이터를 읽기 위하여 읽을 파일을 가리키는 File 혹은 Blob 객체를 이용해 파일의 내용을(혹은 raw data버퍼로) 읽고 사용자의 컴퓨터에 저장하는 것을 가능하게 해준다.
(1) FileReader.onload
FileReader가 성공적으로 파일을 읽어들였을 때 트리거 되는 이벤트 핸들러이다. 이 핸들러 내부에 우리가 원하는 이미지 프리뷰 로직을 넣어주면 된다.
(2) FileReader.readystate
FileReader의 현재 상태를 나타낸다.
(3) FileReader.readAsDataURL()
readAsDataURL은 File 혹은 Blob 을 읽은 뒤 base64로 인코딩한 문자열을 FileReader 인스턴스의 result라는 속성에 담아준다. 이 메서드를 사용해 이미지를 base64로 인코딩하여 Image라는 state 안에 넣어주는 것이다.
구현 모습
플러스 버튼을 누르면 이렇게 파일 찾기가 뜬다.
3. 파일 삭제하는 기능 구현하기
import styles from "./ProfileChange.module.css";
import {useRef, useState} from "react";
import DeleteImg from "../../../assets/my-page/setting/profile-delete.png";
import DefaultImg from "../../../assets/my-page/setting/default-background.png";
const ProfileChange = () => {
const [Image, setImage] = useState(DefaultImg);
const [File, setFile] = useState("");
const fileInput = useRef(null);
const onChange = (e) => {
if (e.target.files[0]) {
setFile(e.target.files[0]);
} else {
setImage(DefaultImg);
return;
}
const reader = new FileReader();
reader.onload = () => {
if (reader.readyState === 2) {
setImage(reader.result);
}
};
reader.readAsDataURL(e.target.files[0]);
};
return (
<div className={styles["profile-setting-main-profile-change-container"]}>
<p className={styles["profile-setting-main-profile-change-title"]}>프로필 변경</p>
<div className={styles["profile-setting-main-profile-change-img-box"]}>
<img src={Image} alt="프로필 사진" className={styles["profile-setting-main-profile-change-img"]} />
<label className={styles["profile-setting-main-profile-change-add-img"]} htmlFor="input-file">
<span className={styles["profile-setting-main-profile-change-add-img-icon"]}>블로그 이미지 찾아보기</span>
<input
type="file"
accept="image/*"
id="input-file"
className={styles["profile-setting-main-profile-change-add-img-input"]}
onClick={() => {
fileInput.current.value = null;
fileInput.current.click();
}}
ref={fileInput}
onChange={onChange}
/>
</label>
<button
type="button"
className={styles["profile-setting-main-profile-change-delete"]}
// (2) 번 설명
onClick={() => {
window.confirm("이미지를 삭제하겠습니까?") ? setImage(DefaultImg) : null;
}}
>
// (1) 번 설명
{Image === DefaultImg ? null : <img src={DeleteImg} alt="이미지 삭제" className={styles["profile-setting-main-profile-change-delete-img"]} />}
</button>
</div>
</div>
);
};
export default ProfileChange;
// (1) 이미지가 DefaultImg(기본 회색이미지)이면, 버튼이 뜨지 않게 하고, DefaultImg(기본 회색이미지)가 아니면, 즉 사진이 업로드 된다면, "이미지 삭제 버튼 아이콘"이 우측 상단에 뜨게끔 한다.
▶ {Image === DefaultImg ? null : <img src={DeleteImg} alt="이미지 삭제" className={styles["profile-setting-main-profile-change-delete-img"]} />}
// (2) 삭제 버튼에 클릭이벤트를 달아서,
삭제 버튼을 눌렀을 때 confirm창이 뜨고, 확인을 누르면 DefaultImg가 뜨게 하고, 취소를 누르면 원래 상태로 유지되게 한다.
▶ onClick={() => { window.confirm("이미지를 삭제하겠습니까?") ? setImage(DefaultImg) : null; }}
확인을 눌렀을 때 삭제되는 모습
취소를 눌렀을때 유지되는 모습
4. 완성된 프로필 업로더