โœ๐Ÿป [Code Camp_TIL] 19์ผ์ฐจ: Storage, ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ, useRef, label & htmlFor

code_Jยท2023๋…„ 4์›” 22์ผ
0

TIL

๋ชฉ๋ก ๋ณด๊ธฐ
25/41
post-thumbnail

์ด๋ฏธ์ง€ ํ”„๋กœ์„ธ์Šค ์ดํ•ด

์˜ค๋Š˜์€ ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•˜๊ณ  ๋ถˆ๋Ÿฌ์˜ค๋Š” ๊ณผ์ •์— ๋Œ€ํ•ด ๊ณต๋ถ€ํ–ˆ๋‹ค.

Storage

์ด๋ฏธ์ง€๋Š” ์Šคํ† ๋ฆฌ์ง€ ์ปดํ“จํ„ฐ๋ผ๋Š” ์ปดํ“จํ„ฐ์— ๋”ฐ๋กœ ์ €์žฅํ•œ๋‹ค. ์Šคํ† ๋ฆฌ์ง€๋Š” ์—ฌ๋Ÿฌ ์ปดํ“จํ„ฐ๋“ค์„ ์—ฐ๊ฒฐ์‹œ์ผœ ๋†“์€ ํฐ ์šฉ๋Ÿ‰์„ ๋‹ด์„ ์ˆ˜ ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋‹ค.

์Šคํ† ๋ฆฌ์ง€๋Š” ๋น„์šฉ์ด ๋งŽ์ด ๋“ค๊ธฐ ๋•Œ๋ฌธ์— ์Šคํ† ๋ฆฌ์ง€๋ฅผ ์ œ๊ณต(cloud provider)ํ•˜๋Š” ํด๋ผ์šฐ๋“œ ์ œ๊ณต์—…์ฒด(ex. AWS, GCP, Azure...)๊ฐ€ ๋“ฑ์žฅํ–ˆ๋‹ค.


์ด๋ฏธ์ง€ ์—…๋กœ๋“œ

๋ธŒ๋ผ์šฐ์ €์—์„œ api๋ฅผ ์š”์ฒญํ•  ๊ฒฝ์šฐ ์ด๋ฏธ์ง€ ์ฃผ์†Œ๋ฅผ ๋ฐฑ์—”๋“œ๋กœ ๋ณด๋‚ด๊ณ , ๋ฐฑ์—”๋“œ๋Š” ํŒŒ์ผ์„ ์Šคํ† ๋ฆฌ์ง€ ์ปดํ“จํ„ฐ๋กœ ๋ณด๋‚ธ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์Šคํ† ๋ฆฌ์ง€ ์ปดํ“จํ„ฐ์—์„œ ์ฃผ์†Œ๋ฅผ ๋‹ค์‹œ ๋ฌธ์ž์—ด๋กœ ๋ฐ›์•„์™€์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅํ•˜๊ฒŒ ๋œ๋‹ค.


์ด๋ฏธ์ง€ ์กฐํšŒ

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์•ˆ์— ์žˆ๋Š” ์ด๋ฏธ์ง€์˜ ์ฃผ์†Œ๋ฅผ ๋ฐ›์•„์™€์„œ img tag์— ๋„ฃ์–ด์ฃผ๋ฉด, ํ•ด๋‹น ์Šคํ† ๋ฆฌ์ง€์— ์ ‘์†ํ•ด์„œ ์ด๋ฏธ์ง€(ํŒŒ์ผ)๋ฅผ ๋‹ค์šด๋กœ๋“œ ๋ฐ›์•„์˜จ๋‹ค.


์ฐธ๊ณ ! ์ด๋ฏธ์ง€๋ฅผ ํฌ๊ฒŒ ํ™•๋Œ€ํ•ด๋ณด๋ฉด ํ”ฝ์…€์ด ํ•˜๋‚˜ํ•˜๋‚˜ ๋ชจ์—ฌ์žˆ๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. ํ”ฝ์…€ ํ•œ ์นธ์€ ๋น›์˜ 3์›์ƒ‰(RGB)๋ฅผ ์„ž์–ด์„œ ๋งŒ๋“ค์–ด์กŒ๋‹ค. ๊ฐ ํ”ฝ์…€์€ 3๊ฐ€์ง€ ์ˆซ์ž๋กœ ๊ตฌ์„ฑ๋œ๋‹ค.

R(0~255)
G(0~255)
B(0~255)


์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์‹ค์Šต


์•„ํด๋กœ ์—…๋กœ๋“œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜ ๋ฐ ์„ธํŒ…

  1. url์„ ๊ฐ€์ง€๊ณ  ์˜ค๊ธฐ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ createUploadLink๋ฅผ ์„ค์น˜

    yarn add apollo-upload-client
    yarn add --dev @types/apollo-upload-client


  1. app.tsx ์„ธํŒ…
// createUploadLink import 
import {createUploadLink} from "apollo-upload-client"

// ํ•จ์ˆ˜ ๋งŒ๋“ค๊ธฐ
const uplodLink = createUploadLink({
		uri : "๋ฐฑ์—”๋“œ ์ฃผ์†Œ"
	})

const client = new ApolloClient({
		link : ApolloLink.from([uplodLink]),
		cache : new inMemoryCache(),
	})

ํŒŒ์ผ ์—…๋กœ๋“œํ•  ํ™”๋ฉด ๊ทธ๋ฆฌ๊ธฐ

// ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ api ์‚ฌ์šฉ์„ ์œ„ํ•œ ์ฟผ๋ฆฌ ์ž‘์„ฑ
const UPLOAD_FILE = gql`
  mutation uploadFile($file: Upload!) {
    uploadFile(file: $file) {
      url
    }
  }
`;

export default function ImageUploadPage(): JSX.Element {
  const [imageUrl, setImageUrl] = useState("");
  const [uploadFile] = useMutation<
    Pick<IMutation, "uploadFile">,
    IMutationUploadFileArgs
  >(UPLOAD_FILE);

  // ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ํ•จ์ˆ˜
  const onChangeFile = async (
    event: ChangeEvent<HTMLInputElement>
  ): Promise<void> => {
    const file = event.target.files?.[0]; 
    // ๋ฐฐ์—ด์„ ๋ฐ›์•„์˜ค๋Š” ์ด์œ : <input type="file" multiple/>์ผ๋•Œ, ์—ฌ๋Ÿฌ ๊ฐœ ์—…๋กœ๋“œ ๊ฐ€๋Šฅํ•˜๊ธฐ ๋•Œ๋ฌธ!
    const result = await uploadFile({ variables: { file } })
    setImageUrl(result.data?.uploadFile.url ?? "");
  };
  
  return (
    <>
    // input type file๋กœ ์ง€์ •
      <input onChange={onChangeFile} type="file" />
      <img src={`https://storage.googleapis.com/${imageUrl}`} />
    </>
  );

์ด๋ฏธ์ง€๋ฅผ ๋ณด๋‚ด๊ณ , url์„ ๋ฐ›์•„์˜ค๋Š” ๊ณผ์ •
1. onChange๋ฅผ ํ†ตํ•ด ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ๊ฐ€์ง€๊ณ  ์™€ file์— ๋„ฃ์–ด์คŒ
2. file์„ variables์— ๋„ฃ์–ด uploadFile api ์š”์ฒญ
3. ์ด๋ฏธ์ง€๊ฐ€ ์ •ํ•ด๋‘” storage์— ๋“ค์–ด๊ฐ€๊ณ , ํ•ด๋‹น storage์—์„œ url ๋ฐ˜ํ™˜ ๋ฐ›์•„์˜ด

์ฐธ๊ณ ! ํ•ด๋‹น url์˜ ์ด๋ฏธ์ง€๋ฅผ ๋ณด๊ณ  ์‹ถ๋‹ค๋ฉด, ์ €์žฅํ•ด๋‘” ์Šคํ† ๋ฆฌ์ง€ ์ฃผ์†Œ ๋’ค์— ์ด๋ฏธ์ง€ url์„ ์ ์œผ๋ฉด ๋œ๋‹ค!

์ด๋ฏธ์ง€๋ฅผ ๊ฒŒ์‹œ๊ธ€์— ๋“ฑ๋กํ•˜๊ธฐ


HTMl ๊ธฐ๋ณธ file ํƒœ๊ทธ ์ˆจ๊ธฐ๊ธฐ

๊ธฐ๋ณธ input tag๋Š” ์˜ˆ์˜์ง€ ์•Š๋‹ค.. ์ข€ ๋” ์˜ˆ์˜๊ฒŒ UI๋ฅผ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” input ํƒœ๊ทธ๋ฅผ ์ˆจ๊ธฐ๊ณ  ๋‹ค์Œ์˜ 2๊ฐ€์ง€ ๋ฐฉ๋ฒ• ์ค‘ ์ƒํ™ฉ์— ๋งž๋Š” ๋ฐฉ์‹์„ ํ™œ์šฉํ•˜๋ฉด ๋œ๋‹ค!


useRef ํ™œ์šฉํ•˜๊ธฐ

์šฐ๋ฆฌ๊ฐ€ HTML ํƒœ๊ทธ๋ฅผ ์„ ํƒํ•  ๋•Œ๋Š” getElementId๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค. react์—์„œ๋Š” HTML ํƒœ๊ทธ์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•ด useRef๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

// useRef import
import { useRef } from 'react';

export default function ImageRefPage(): JSX.Element {
  // ref ๊ธฐ๋ณธ ์„ค์ •
  // fileRef๋ฅผ input ํƒœ๊ทธ์— ์—ฐ๊ฒฐํ•ด์ฃผ๋ฉด ํ•ด๋‹น ํƒœ๊ทธ๋Š” fileRef๋กœ ๋ถˆ๋Ÿฌ์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ!
	const fileRef = useRef<HTMLInputElement>(null);
}

return (
  <>
<input
  style={{ display: "none" }}
  onChange={onChangeFile}
  type="file"
// input ํƒœ๊ทธ๋ฅผ fileRef๋ฅผ ์ด์šฉํ•ด ์‚ฌ์šฉ ๊ฐ€๋Šฅ
  ref={fileRef}
/>
</>
)

์ด์ œ useRef์˜ ๊ธฐ๋Šฅ ์ค‘ current๋ฅผ ์‚ฌ์šฉํ•ด์„œ current ์•ˆ์— click์ด๋ผ๋Š” ๊ธฐ๋Šฅ์„ onClickImage ํ•จ์ˆ˜์— ๋„ฃ์–ด์ค€๋‹ค. current๋Š” fileRef์— ๋“ค์–ด์˜จ ํƒœ๊ทธ๋ฅผ ๋œปํ•˜๊ณ , ๊ทธ ํƒœ๊ทธ๋ฅผ clickํ•˜๊ฒ ๋‹ค๋Š” ๊ธฐ๋Šฅ์ด๋‹ค!

const onClickImage = (): void => {
    // ๊ธฐ์กด ๋ฐฉ์‹: document.getElementById("ํŒŒ์ผํƒœ๊ทธID")?.click();
    fileRef.current?.click();
  };

์ด์ œ button tag๋ฅผ ๋งŒ๋“ค์–ด onClickImage ํ•จ์ˆ˜์™€ ๋ฐ”์ธ๋”ฉ์‹œ์ผœ์ฃผ๋ฉด ๋œ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด button์„ ํด๋ฆญํ–ˆ์„ ๋•Œ fileRef.current.click()์ด ์‹คํ–‰๋˜๊ณ , ์šฐ๋ฆฌ๊ฐ€ useRef์— ๋„ฃ์–ด๋‘์—ˆ๋˜ input ํƒœ๊ทธ๋ฅผ ํด๋ฆญํ•œ ๊ฒƒ๊ณผ ๋™์ผํ•œ ๊ฒฐ๊ณผ๊ฐ€ ์‹คํ–‰๋œ๋‹ค!

<button onClick={onClickImage}>์ด๋ฏธ์ง€ ๋“ฑ๋ก ๋ฒ„ํŠผ</button>

์ด์ œ img ํƒœ๊ทธ์—๋„ onClickImage ํ•จ์ˆ˜๋ฅผ ๋ฐ”์ธ๋”ฉํ•˜๊ณ , input ํƒœ๊ทธ์˜ ์†์„ฑ์œผ๋กœ hidden์„ ๋„ฃ์–ด์„œ input ํƒœ๊ทธ๋Š” ๋ธŒ๋ผ์šฐ์ €์— ๋‚˜ํƒ€๋‚˜์ง€ ์•Š๋„๋ก ํ•˜๊ณ , img์™€ button์„ ํด๋ฆญํ–ˆ์„ ๋•Œ input type=file ์„ ํด๋ฆญํ•œ ๊ฒƒ๊ณผ ๋™์ผํ•œ ๊ฒฐ๊ณผ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

return (
		<>
			<div>
				<img onClick={onClickImage} style={{ width: '500px' }} id="image" />
				<input
					hidden={true}
					ref={fileRef}
					type="file"
					onChange={readImage}
				></input>
				<button onClick={onClickImage}>์ด๋ฏธ์ง€ ๋“ฑ๋ก ๋ฒ„ํŠผ</button>
			</div>
		</>
	);

label ํƒœ๊ทธ์™€ htmlFor

useRef ์™ธ์— input ํƒœ๊ทธ๋ฅผ ์ˆจ๊ธฐ๊ณ  UI๋ฅผ ๊ฐœ์„ ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ label ํƒœ๊ทธ๋ฅผ ์ด์šฉํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

label ํƒœ๊ทธ์—๋Š” htmlFor ์ด๋ผ๋Š” ์†์„ฑ์ด ์žˆ๋‹ค. htmlFor์— ๊ฐ’์„ ๋„ฃ์œผ๋ฉด ๊ฐ’๊ณผ ๋˜‘๊ฐ™์€ id๋ฅผ ์ฐพ์•„์„œ ๊ทธ ํƒœ๊ทธ์˜ ๊ธฐ๋Šฅ๊ณผ ์—ฐ๊ฒฐํ•ด์ค€๋‹ค.

์•„๋ž˜ ์ฝ”๋“œ์™€ ๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

<div>
	<label htmlFor="fileTag">์ด๊ฑฐ ๋ˆŒ๋Ÿฌ๋„ ์‹คํ–‰๋ผ์š”!</label>
	<img style={{ width: '500px' }} id="image" />
	<input id="fileTag" type="file" onChange={readImage}></input>
</div>

์ด์ œ label ํƒœ๊ทธ๋ฅผ ์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” ์˜ˆ์œ ๋””์ž์ธ์œผ๋กœ ๋ฐ”๊พธ๊ณ , ๊ธฐ์กด input ํƒœ๊ทธ๋Š” ์•ˆ ๋ณด์ด๋„๋ก CSS๋ฅผ ์ž‘์—…ํ•˜๋ฉด ๋œ๋‹ค.



image validation

์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•  ๋•Œ ์šฉ๋Ÿ‰์„ ์ œํ•œํ•˜๊ฑฐ๋‚˜ ํŒŒ์ผ ํ™•์žฅ์ž๋ฅผ ์ œํ•œ ๋ฐ ๊ฒ€์ฆํ•˜๋Š” ๊ณผ์ •์ด ํ•„์š”ํ•  ๋•Œ์—๋Š” ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ• ๊นŒ?


์ด๋ฏธ์ง€ ์œ ๋ฌด์™€ ์‚ฌ์ด์ฆˆ ๊ฒ€์ฆ

const onChangeFile = async (
    event: ChangeEvent<HTMLInputElement>
  ): Promise<void> => {
    const file = event.target.files?.[0];

		// ๊ฒ€์ฆ ๋กœ์ง
    if (typeof file === "undefined") {
      alert("ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค.");
      return;
    }
  // ์ด๋ฏธ์ง€ ์‚ฌ์ด์ฆˆ 5MB ์ดํ•˜๋กœ ์ œํ•œ
    if (file.size > 5 * 1024 * 1024) { 
      alert("ํŒŒ์ผ ์šฉ๋Ÿ‰์ด ๋„ˆ๋ฌด ํฝ๋‹ˆ๋‹ค.(์ œํ•œ: 5MB)");
      return;
    }

		// API ํ˜ธ์ถœ 
    const result = await uploadFile({ variables: { file } });
    setImageUrl(result.data?.uploadFile.url ?? "");
  };

์ด๋ฏธ์ง€ ํ™•์žฅ์ž ๊ฒ€์ฆ

์ฒซ ๋ฒˆ์งธ ๋ฐฉ๋ฒ•

์‚ฌ์ง„ ์„ ํƒ ์‹œ ํ˜ธ์ถœ๋˜๋Š” ํ•จ์ˆ˜์—์„œ ๊ฒ€์ฆํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค.

const onChangeFile = async (
    event: ChangeEvent<HTMLInputElement>
  ): Promise<void> => {
    const file = event.target.files?.[0];

		~~~	// ๊ธฐํƒ€ ๊ฒ€์ฆ ๋กœ์ง ์ƒ๋žต

    if (!file.type.includes("jpeg") && !file.type.includes("png")) {
      alert("jpeg ๋˜๋Š” png ํŒŒ์ผ๋งŒ ์—…๋กœ๋“œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.");
      return;
    }

		~~~ // API ํ˜ธ์ถœ ๋กœ์ง ์ƒ๋žต

  };

๋‘ ๋ฒˆ์งธ ๋ฐฉ๋ฒ•

input ํƒœ๊ทธ์˜ accept ์†์„ฑ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ๋‹ค.

<input
  style={{ display: "none" }}
  onChange={onChangeFile}
  type="file"
  ref={fileRef}
  accept="image/jpeg,image/png" 
  // ๋„์–ด์“ฐ๊ธฐ ์—†์ด ์ฝค๋งˆ(,)๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ž‘์„ฑ
  // accept๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ์ง€์ •๋˜์ง€ ์•Š์€ ํ™•์žฅ์ž๋Š” ์„ ํƒ ๋ถˆ๊ฐ€
/>

์ด๋ฏธ์ง€ ๊ฒ€์ฆ ๋กœ์ง์€ ๋”ฐ๋กœ ์ปดํฌ๋„ŒํŠธ๋กœ ๋ถ„๋ฆฌํ•˜๋ฉด import๋งŒ ํ•ด์„œ ํŽธ๋ฆฌํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค!



์ปดํ“จํ„ฐ์—์„œ์˜ ์ˆซ์ž


์œ„์—์„œ ์ด๋ฏธ์ง€ ์šฉ๋Ÿ‰์„ 5MB๋กœ ์ œํ•œํ•  ๋•Œ, 5MB๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋‚˜ํƒ€๋‚ด์คฌ๋‹ค.

5 * 1024 * 1024

์šฉ๋Ÿ‰์„ ์ฝ”๋“œ๋กœ ์ž‘์„ฑํ•  ๋•Œ์—๋Š” ์•„๋ž˜ ๊ทธ๋ฆผ์„ ์ฐธ๊ณ ํ•ด์„œ ์œ„์˜ ์ฝ”๋“œ์™€ ๊ฐ™์ด ๋‚˜ํƒ€๋‚ด์ฃผ๋ฉด ๋œ๋‹ค!


profile
Web FE ๊ฐœ๋ฐœ์ž ์ทจ์ค€์ƒ

0๊ฐœ์˜ ๋Œ“๊ธ€