프로젝트 환경
- Next JS _app router
- TypeScript
- Style : TailWind
중급 프로젝트에서 모달 UI를 맡아서 작업을 하고 있다.
다른 모달들은 단순한 UI이기도 하고, 크게 고민할 요소조차 없었다. 그냥 하면 되는 것들.
그런데도, 생각보다 시간이 꽤나 걸렸다.
이유가 뭘까, 왜이렇게 속도가 안날까 곰곰히 생각을 해봤다.
그 이유는 절대적인 경험 부족이다.
다양한 input type을 구현 해본적이 없었고, 막연하게 쉽게 할 수 있을 것이라고 자만하고 있었다.
재사용을 고민하다보니, 공통 input으로 빼기도 애매했고 생각보다 고민에 오랜 시간이 걸렸다.
그 중에서도 내가 턱! 하고 막히게 된 input은 바로 file 타입을 가진 input이다.
내가 원하는 대로 input을 커스터마이징 하는게 너무 재미있었고, 새로운 지식을 또 얻게되서 이렇게 포스팅을 쓴다.
기본적인 input type="file"의 모습은 아래처럼 생겼다.
직관적임의 끝판왕.
그런데 우리가 원한 건 이게 아니니까 바꿔보자.
우선, label을 눌러도 input이 활성화되는 걸 생각해서 label 안에 넣어보려고 했다.
그리고 원래의 input을 display : none 시키면 비슷하게 되지 않을까?
// className 안의 요소는 tailwind의 스타일 속성이다.
<div className='flex flex-col gap-2.5 '>
<label htmlFor='image-upload' className='flex flex-col gap-2.5 font-extrabold text-lg'>
<p className='flex gap-1 font-extrabold text-lg'>이미지</p>
// 내가 원하는 버튼
<div className='bg-[#f5f5f5] rounded-md flex justify-center items-center p-6 w-[4.75rem] h-[4.75rem]'>
<div className='relative w-7 h-7'>
<Image fill src='/icon/violet_plus.svg' alt='이미지 추가하기' id='input-image' />
</div>
</div>
</label>
<input type='file' id='image-upload' accept='image/*' className='hidden' onChange={handleImageChange} />
</div>
좋다 좋아! 그러면 이제, 이미지를 선택했을 때 선택한 이미지가 저 네모 박스에 들어가도록 해야한다.
근데, 나는 첨부한 파일을 어디서 가져올 수 있는지를 알지 못했다.
그럼 알아보면 되징!
오호! 공식문서를 통해 input의 value 값을 가져오듯이 value를 사용해도 된다는 것과 files를 사용해도 된다는 것을 알게되었다.
그렇다면 파일을 선택했을 때 files를 setState 값에 넣고 그 값을 사용해서 저 버튼이 바뀌도록 하면 되겠다고 생각했다.
// 코드 이해를 위해 tailwind를 임의로 제거했다
const CreateWorkModal = () => {
const [selectImage, setSelectImage] = useState(''); // 업로드한 이미지 파일을 담을 state
const handleImageChange = (event: ChangeEvent<HTMLInputElement>) => {
// onChange 이벤트로 state를 바꾸는 함수를 정의한다.
const file = event.target.value;
setSelectImage(file);
};
return (
<ModalLayout>
<form>
//...
<div>
<p>이미지</p>
<label htmlFor='image-upload'>
// 이미지 첨부를 안했을 때는 + 버튼이 렌더링 되고, 첨부하면 첨부한 이미지로 렌더링되도록 함
{!selectImage ? (
<div>
<div>
<Image fill src='/icon/violet_plus.svg' alt='이미지 추가하기' id='input-image' />
</div>
</div>
) : (
<>
<div>
<div>
<Image fill src='/icon/edit.svg' alt='이미지 변경하기' />
</div>
<div>
// 선택된 이미지 경로에 따라 src가 바뀌도록 함
<Image fill src={selectImage} alt='이미지 추가하기' />
</div>
</>
)}
</label>
<input type='file' id='image-upload' accept='image/*' onChange={handleImageChange} />
</div>
</form>
</ModalLayout>
);
};
하지만, 나를 반긴 것은 에러 창이었다. ㅠㅠ
src 경로가 fakepath로 되어 있어서 이미지 파일을 찾지 못하는 것이다.
흠... 정확히 보기 위해서 event.target.value 를 콘솔에 좀 찍어봤다.
C:\fakepath\유튜브_기본프로필_파랑.jpg
왜 이렇게 경로를 숨겨서 보여주는 걸까?
fakepath로 숨기는 이유
실제 파일 경로를 바로 보여주지 않는 것은 사용자의 파일 정보를 지키기 위한 일종의 '보안상의 이유' 이다.
인터넷 익스플로어에서는 fakepath를 사용하지 않는다는 사실!
이 놈들은 보안 따위 신경쓰지 않았었나 보다.
그렇다면...나의 image 경로는 대체 어떻게 가져와야한단 말이냐...!
더 많은 정보가 필요하다.
새로운 정보가 필요하다!!!!
fakepath로 숨겨진 파일 가져오기
getElementById를 이용하여 tag의 src에 경로를 추가하는 방법도 있었지만, input 태그와 Image 태그를 모두 가져와서 작업을 해줘야 했기 때문에 더 간결하게 작성할 수 있는 FileReader을 이용하기로 결정했다.
FileReader은 웹 애플리케이션이 비동기적으로 데이터를 읽기 위해 읽을 파일을 가리키는 File 혹은 Blob 객체를 이용해 파일을 읽고 사용자의 컴퓨터에 저장하는 것을 가능하게 해주는 객체이다.
-> 업로드한 파일을 저장하게 해주는 것 (다른 input의 value 포함)
File 객체는 <input> 태그를 이용해 유저가 업로드한 파일들의 결과로 반환된 FileList 객체, 드라그 앤 드랍으로 반환된 DataTransfer 객체, HTMLCanvasElement의 mozGetAsFile() API로부터 얻는다.
-> 나는 <input type="file" /> 에서 업로드된 파일이 files 대상이 될 것이다.
// 생성자 함수로 생성할 수 있다.
const reader = new FileReader();
FileReader 객체의 많은 이벤트 핸들러 중에서 읽기가 성공적으로 끝났을 때 실행하는 onload 이벤트를 사용하면 될 것으로 보였다.
그럼 파일 내용을 반환하는 결과를 어떻게 보지?
-> result 라는 인스턴스 속성을 이용하면 파일의 내용을 반환 받을 수 있다.
그럼 이제 fakepath가 아닌 파일 내용을 반환 받을 수 있으니, 핸들러 함수를 작성해서 setState 값을 넣어주자
그 전에, 값을 setState에 넣어줬지만, 이미지에 src를 사용하려면 사용하기 위해서 변환해주는 작업이 필요하다.
이때 사용할 수 있는 것은 readAsDataURL이라는 인스턴스 메서드이다.
이 메서드는 입력 받은 이미지 파일을 텍스트로 변환해주는 역할을 한다.
파일을 사용하기 위한 준비는 정말 다 끝났다.
코드를 작성해보자 !
//...
const handleImageChange = (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const reader = new FileReader(); //생성자 함수를 통해 생성
// onload 이벤트 핸들러 함수를 통해 파일 내용을 setState에 넣는다.
reader.onload = () => {
setSelectImage(reader.result as string);
};
// 이미지 파일을 텍스트로 사용할 수 있게 변환해준다.
reader.readAsDataURL(file);
}
};
//...
<div>
<label htmlFor='image-upload'>
<p>이미지</p>
{!selectImage ? (
<div>
<div>
<Image fill src='/icon/violet_plus.svg' alt='이미지 추가하기' id='input-image' />
</div>
</div>
) : (
<>
<div>
<div>
<Image fill src='/icon/edit.svg' alt='이미지 변경하기' />
</div>
<div/>
<Image fill src={selectImage} alt='이미지 추가하기' />
</div>
</>
)}
</label>
<input type='file' id='image-upload' accept='image/*'onChange={handleImageChange} />
</div>
주석을 읽다보면, setState에 값을 넣어주는 onload를 실행한 뒤에 사용할 수 있는 형태로 변환하지? 하는 생각이 들 수 있다.
만약 그런 생각이 들었다면 " fakepath로 숨겨진 파일 가져오기 " 의 첫 줄을 다시 한 번 읽어보면 방법을 찾을 수 있을 것이다.
그래도 모르겠다면? 함수의 실행 및 동작 순서를 통해 이해해보자.
1. 사용자가 이미지 파일을 선택한다.
2. input 태그의 onChange 이벤트가 발생하여, handleImageChange 함수가 호출된다. 이때 선택한 파일은 event.target.files 로부터 가져온다.
3. 선택한 파일이 있는지를 확인하기 위해 'event.target.files?.[0]을 사용하여 파일 목록에서 선택된 파일을 가져오고, 만약 선택된 파일이 없다면 함수 실행이 중지된다.
4. 파일이 있으면 reader 이라는 이름으로 FileReader 객체를 생성한다.
5. FileReader 객체의 'onload' 이벤트 핸들러를 설정한다. 이 핸들러는 FileReader가 읽기 작업을 완료하고 데이터를 성공적으로 읽어왔을 때 호출된다.
6. 'reader.readAsDataURL(file)'을 호출하여 RileReader에게 파일을 읽어서 데이터 URL로 변환하도록 한다. 이 과정은 비동기적으로 이루어지기 때문에 파일을 읽는 동안 다음 코드가 실행된다.
7. 파일을 읽는동안 'reader.onload' 이벤트 핸들러는 대기한다.
8. FileReader가 파일을 읽은 후, 'reader.onload' 이벤트 핸들러가 호출된다.
9. 호출된 이벤트 핸들러에서는 FileReader의 'result' 속성을 사용하여 이미지의 데이터 URL을 가져온다. 이 데이터 URL은 Base64로 인코딩된 이미지 데이터를 포함한다.
10. 'setSelectImage(reader.result as string)' 을 호출하여 이미지 데이터 URL을 setState 값에 저장한다. 이로써 React 컴포넌트가 다시 렌더링되고, 선택한 이미지가 화면에 로드된다.
6번에서 그 다음 과정으로 넘어가는 것을 보면 함수의 실행 흐름이 이해되었을 것이라고 생각한다.
그럼 완성된 결과물을 한 번 보자!
어찌보면 이미지 input 하나 작업한 것이기는 하지만, 블로그를 통해 정리하면서 내가 작성한 코드를 더욱 더 이해할 수 있는 시간이었다.
내일부터는 페이지 단위의 작업을 시작하는데, 어떤 고민을 하며 코드를 짜게 될 지 기대할 수 있을 것 같다.
막히더라도 늘 해답은 있고, 발견해내는 과정은 값지니 더 열심히 해보자!
'💡뚝딱뚝딱 만들어보자 ~! :) > Taskify' 카테고리의 다른 글
[PagiNation] UI와 로직 분리하기 (1) | 2024.05.01 |
---|