๐Ÿ‘ FE

๊ธ€ ์‚ฌ์ด ์ด๋ฏธ์ง€ ๋„ฃ๊ณ  ๊ด€๋ฆฌํ•˜๊ธฐ

์š”๋‹ˆ๊น€ 2023. 9. 3. 22:32

๐Ÿ‘   ๋ฐฐ๊ฒฝ

์‚ฌ์ด๋“œ ํ”„๋กœ์ ํŠธ์—์„œ ๊ธ€ ์‚ฌ์ด์— ์ด๋ฏธ์ง€๋ฅผ ๋„ฃ๊ณ  ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒŒ์‹œํŒ CRUD ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค์–ด์•ผ ํ–ˆ๋‹ค.

์ด๋ฏธ์ง€, ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ ๊ด€๋ฆฌ ๋ฐฉ์‹์— ๋Œ€๊ทœ๋ชจ ์ˆ˜์ •์ด ์—ฌ๋Ÿฌ ๋ฒˆ ์žˆ์—ˆ๊ณ … ์ตœ์ข… ์„ ํƒํ•œ ๋ฐฉ์‹๊ณผ ํ•จ๊ป˜ ์ ‘๊ทผํ–ˆ๋˜ ๋ฐฉ์‹์„ ์ •๋ฆฌํ•ด๋ณด๊ณ  ์‹ถ์–ด์„œ ํฌ์ŠคํŒ…์„ ์ž‘์„ฑํ•œ๋‹ค..!

 

 

๐Ÿ‘   ์ƒ๊ฐํ–ˆ๋˜ ๋ฐฉ๋ฒ•๋“ค…

  1. ์ด๋ฏธ์ง€๋ฅผ ์„ ํƒํ•  ๋•Œ๋งˆ๋‹ค s3 ์Šคํ† ๋ฆฌ์ง€์— ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•˜๊ณ  → ์‘๋‹ต์œผ๋กœ ๋„˜๊ฒจ์ค€ ์ฃผ์†Œ๋ฅผ img ํƒœ๊ทธ๋กœ ์ถ”๊ฐ€ํ•˜์—ฌ ํ”„๋ก ํŠธ์— ๋ณด์—ฌ์ฃผ๋Š” ๋ฐฉ๋ฒ•
    • ๊ธ€ ์ž‘์„ฑ์„ ์™„๋ฃŒํ•˜๊ธฐ๋„ ์ „์— ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ → ์™„๋ฃŒ ์ „ ์ทจ์†Œํ•œ๋‹ค๋ฉด ๋‚ญ๋น„๊ฐ€…
  2. ์ด๋ฏธ์ง€ ๋ฐ ๊ธ€ ์ „์ฒด ๋‹ค ์ „์†ก
    • ์ผ๋ฐ˜์ ์œผ๋กœ ์›น ์„œ๋ฒ„์—์„œ request body ์‚ฌ์ด์ฆˆ ์ œํ•œ์„ ๊ฑธ์–ด์„œ ์šด์šฉํ•˜๊ธฐ์— ๋Œ€์šฉ๋Ÿ‰ ๋˜๋Š” ์—ฌ๋Ÿฌ ํŒŒ์ผ ์—…๋กœ๋“œ ์‹œ ๋ฌธ์ œ๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋‚œ๋‹ค…
    • ์ผ๋ฐ˜์ ์œผ๋กœ ์Šคํ† ๋ฆฌ์ง€๋Š” ๊ณต์šฉ ์‚ฌ์šฉ์„ ์œ„ํ•ด ์„œ๋ฒ„์™€ ๋ณ„๊ฐœ๋กœ ๊ตฌ์ถ•ํ•œ๋‹ค๊ณ  ํ•จ…

 

๐Ÿ‘   ๊ทธ๋ž˜์„œ ์ฑ„ํƒํ•œ ๋ฐฉ๋ฒ•์€…

  1. ๊ธ€ ์ƒ์„ฑ ์ „๊นŒ์ง€ ์ž‘์„ฑํ•œ ํ…์ŠคํŠธ์™€ ์„ ํƒํ•œ ์ด๋ฏธ์ง€๋ฅผ base64๋กœ ์ธ์ฝ”๋”ฉํ•˜์—ฌ ํ”„๋ก ํŠธ์—์„œ ๋ณด์—ฌ์ฃผ๊ณ  / ํŽธ์ง‘ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•จ
  2. ๊ธ€ ์ž‘์„ฑ ์‹œ์ ์— ์ด๋ฏธ์ง€ ์œ ๋ฌด๋ฅผ ๋ถ„๋ฆฌํ•˜์—ฌ
  3. ์ด๋ฏธ์ง€๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ๊ธ€ ์ƒ์„ฑ api ํ˜ธ์ถœ
  4. ์ด๋ฏธ์ง€๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ๊ธ€ ์ƒ์„ฑ api ํ˜ธ์ถœ - ๋ณด์—ฌ์ฃผ๋˜ ์ด๋ฏธ์ง€๋ฅผ form data๋กœ ๋ณ€ํ™˜ ํ›„ s3 ์„œ๋ฒ„์— ์—…๋กœ๋“œ & base 64 url์„ s3 url๋กœ ๋ณ€๊ฒฝ ํ›„ ๊ธ€ ์—…๋ฐ์ดํŠธ api ํ˜ธ์ถœ
    (*์™œ? ๊ฒŒ์‹œ๊ธ€์ด ์ƒ์„ฑ๋  ๋•Œ ๊ฒŒ์‹œ๊ธ€ id๊ฐ€ ์ƒ์„ฑ๋˜๊ณ , ์ด๋ฏธ์ง€๋Š” ๊ฒŒ์‹œ๊ธ€ id๋ฅผ ์ฐธ์กฐํ•˜์—ฌ ์กฐํšŒ/์‚ญ์ œ์— ์‚ฌ์šฉ๋จ)

 

์—…๋กœ๋“œํ•˜๋Š” ๋™์•ˆ ์ปค๋„ฅ์…˜์„ ๊ณ„์† ๋ฌผ๊ณ  ์žˆ์–ด์•ผ ํ•˜๊ณ  ๊ตฌํ˜„์ด ๋ณต์žกํ•˜๋‹ค๋Š” ๋‹จ์ ์€ ์žˆ์—ˆ์ง€๋งŒ…

๋ถˆํ•„์š”ํ•œ ์ž์› ๋‚ญ๋น„๋ฅผ ์ตœ์†Œํ™”ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด๋ผ๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค...

 

 

๐Ÿ‘   ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ–ˆ๋‚˜์š”…

 

1. content editable ์†์„ฑ ์‚ฌ์šฉ

๊ธ€ ์‚ฌ์ด์— ์ด๋ฏธ์ง€๋ฅผ ๋„ฃ๋Š” ๋ฐฉ๋ฒ•์„ ๋ชจ์ƒ‰ํ•˜๋˜ ์ค‘ ์ฐพ์€ ์†์„ฑ์ด๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ๋™์ ์œผ๋กœ ํ…์ŠคํŠธ๋‚˜ ๋‚ด์šฉ์„ ํŽธ์ง‘ํ•  ์ˆ˜ ์žˆ๊ณ , ํ…์ŠคํŠธ, ์ด๋ฏธ์ง€, ๋งํฌ ๋“ฑ์„ ์ˆ˜์ •ํ•˜๊ฑฐ๋‚˜ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด ์ค€๋‹ค. div contenteditable์—๋Š” ์ž…๋ ฅํ•˜๋Š” ์š”์†Œ๋“ค์ด html๋กœ ๋ณ€ํ™˜๋˜์–ด ๋“ค์–ด๊ฐ„๋‹ค.

<Box contentEditable={true}></Box>

 

React๋Š” ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ๋ Œ๋”๋ง ํ•  ๋•Œ ์ž์ฒด์ ์œผ๋กœ ์ด์Šค์ผ€์ดํ”„ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜์—ฌ ํ•ด๋‹น ์ž…๋ ฅ์ด HTML ํƒœ๊ทธ๋กœ ํ•ด์„๋˜์ง€ ์•Š๊ฒŒ ํ•˜์—ฌ ์ž๋™์œผ๋กœ XSS ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€ํ•œ๋‹ค๊ณ  ํ•œ๋‹ค… ํ•˜์ง€๋งŒ contentEditable ์†์„ฑ์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ๋‚ด์šฉ์„ ํŽธ์ง‘ํ•˜๊ณ  ์ˆ˜์ •ํ•˜๋Š” ๊ณผ์ •์—์„œ ์ž…๋ ฅ / ์กฐํšŒ ์š”์†Œ๋“ค์ด html๋กœ ๋ณ€ํ™˜๋˜์–ด ๋“ค์–ด๊ฐ€๋Š” ๊ฒƒ์„ ํ—ˆ์šฉํ•˜๊ณ , ๋•Œ๋ฌธ์— ์•…์˜์ ์ธ ์‚ฌ์šฉ์ž๊ฐ€ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฝ์ž…ํ•˜๊ฑฐ๋‚˜ (XSS ๊ณต๊ฒฉ) ๋‹ค๋ฅธ ๊ณต๊ฒฉ์„ ์‹œ๋„ํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํ•œ๋‹ค… ์ด๋Ÿฐ ์ด์œ  ๋•Œ๋ฌธ์— ๊ธ€ ์‚ฌ์ด ์ด๋ฏธ์ง€ ๋„ฃ๋Š” ์„œ๋น„์Šค๋ฅผ ๋งŽ์ด ๋ชป ๋ณธ ๊ฑด๊ฐ€ ์‹ถ๊ธฐ๋„ ํ•˜๊ณ …

 

 

2. ์ด๋ฏธ์ง€๋ฅผ base64 url๋กœ ๋ณ€๊ฒฝ

type์ด file์ธ input ํƒœ๊ทธ์— ๋‹ด๊ธด ํŒŒ์ผ์„ input.current.files๋กœ ์ถ”์ถœ ํ›„ FileReader๋กœ ํŒŒ์ผ ๋‚ด์šฉ์„ base 64 ๋ฐ์ดํ„ฐ url๋กœ ์ฝ์–ด์˜จ๋‹ค. img ํƒœ๊ทธ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  content editable ์š”์†Œ ๋‚ด ๋™์ ์œผ๋กœ ์ถ”๊ฐ€ํ•ด ์ค€๋‹ค.

function KeyboardFixedElement(){
	const fileInputRef = useRef<HTMLInputElement | null>(null);

	function handleFileChange() {
	  if (
	    fileInputRef.current && 
	    fileInputRef.current.files &&
	    fileInputRef.current.files.length > 0 // ์„ ํƒํ•œ ํŒŒ์ผ์ด ์žˆ๋Š” ๊ฒฝ์šฐ
	  ) {
	    const files = fileInputRef.current.files;
	    for (let i = 0; i < files.length; i++) {
	      const file = files[i];
	      const reader = new FileReader();
	      reader.onloadend = () => { // ํŒŒ์ผ์„ base 64 url๋กœ ๋ณ€ํ™˜
	        if (reader.result) {
	          const newNode = document.createElement("img");
	          newNode.src = reader.result as string;
	          newNode.alt = "ํฌ์ŠคํŒ… ์ด๋ฏธ์ง€";
	          newNode.style.maxWidth = "100%";
		        
						const targetElement = document.getElementById("contentEditable"); 
	          targetElement && targetElement.appendChild(newNode); // ๋ณ€ํ™˜ํ•œ url์„ src๋กœ ๊ฐ–๋Š” ์ด๋ฏธ์ง€ ํƒœ๊ทธ๋ฅผ contenteditable ๋ณธ๋ฌธ์— ์‚ฝ์ž… 
	        }
	      };
	      reader.readAsDataURL(file);
	    }
	  }
	}
	
	return (
	...
	 <input
	      type="file"
	      accept="image/*"
	      ref={fileInputRef}
	      style={{ display: "none" }}
	      onChange={handleFileChange}
	      multiple
	    />
	...
	)
}

 

์ถ”๊ฐ€๋กœ ๋ชจ๋“  ํ•„๋“œ๊ฐ€ ์ž‘์„ฑ ์™„๋ฃŒ๋œ ๊ฒฝ์šฐ์—๋งŒ ์ œ์ถœ ๋ฒ„ํŠผ์„ ํ™œ์„ฑํ™”์‹œํ‚จ๋‹ค๊ณ  ํ–ˆ์„ ๋•Œ, ๋ณธ๋ฌธ ๋‚ด์šฉ(innerHTML) ๊ฐ€๋ณ€๊ฐ’์„ ์ƒํƒœ๊ฐ’์œผ๋กœ ๊ด€๋ฆฌํ•ด์ฃผ๋ ค ํ–ˆ์ง€๋งŒ div์€ ๋‹ค๋ฅธ input, textarea ํƒœ๊ทธ๋“ค์ฒ˜๋Ÿผ onchange ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๊ณ  placeholder๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์—†๋Š” ์ด์Šˆ๊ฐ€ ์žˆ์–ด DOM ์š”์†Œ์˜ ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ๋„๋ก mutation observer๋ฅผ ๋“ฑ๋กํ–ˆ๋‹ค...

function GeneralPostForm(){
	const editableDivRef = useRef<HTMLDivElement | null>(null);
	...
	useEffect(() => {
	    const observerCallback: MutationCallback = (mutationsList, observer) => {
	      for (const mutation of mutationsList) {
	        if (
	          mutation.type === "childList" ||
	          mutation.type === "characterData"
	        ) {
	          handleContentChange(editableDivRef.current?.innerHTML);
	        }
	      }
	    };
	    const observer = new MutationObserver(observerCallback);
	    if (editableDivRef.current) {
	      const observerConfig: MutationObserverInit = {
	        childList: true,
	        subtree: true,
	        characterData: true,
	      };
	      observer.observe(editableDivRef.current, observerConfig);
	    }
	    return () => {
	      observer.disconnect();
	    };
	  }, []);

	...
	return (
		<Box
        contentEditable
				id="contentEditable"
        onBlur={handlePlaceholderChange}
        ref={editableDivRef}
        onFocus={handlePlaceholderChange}
      ></Box>
	)
}

 

3. img base url ์ถ”์ถœ

๋ณธ๋ฌธ ๋‚ด์šฉ์— ์‚ฝ์ž…ํ•œ ์ด๋ฏธ์ง€ Data URL ๋‚ด base64๋กœ ์ธ์ฝ”๋”ฉํ•œ ์ด๋ฏธ์ง€๋ฅผ ์ถ”์ถœํ•˜๊ณ …

// img str ์ถ”์ถœ ํ•จ์ˆ˜
function extractImgBaseStr() {
  const innerHTML = document.querySelector("#contentEditable")!.innerHTML;
  const imgSrcPattern = /data:[^"]+/g; // Data URL ์ •๊ทœํ‘œํ˜„์‹
  const encodedImgLst = innerHTML.match(imgSrcPattern) || [];
  return encodedImgLst;
}

 

4. img url form data ๋ณ€ํ™˜ ์—…๋กœ๋“œ

Data URL์„ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์‹œ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ํŒŒ์ผ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์ „๋‹ฌํ•ด์•ผ ํ•  ํ˜•ํƒœ์˜ form data๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.

function addBase64ImagesToFormData(base64Images: any, formData: any) {
  base64Images.forEach((base64ImageData: any, index: number) => {
    const fileName = `img${index + 1}.jpeg`; /
    const file = dataURLtoFile(base64ImageData, fileName);
    formData.append("multipartFiles", file);
  });
}

function dataURLtoFile(dataurl: string, fileName: string) {
  const arr = dataurl.split(",");
  const mime = arr[0].match(/:(.*?);/)?.[1];
  const bstr = atob(arr[1]);
  let n = bstr.length;
  const u8arr = new Uint8Array(n);
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n);
  }
  return new File([u8arr], fileName, { type: mime });
}

 

s3 ์„œ๋ฒ„์— ์˜ฌ๋ฆฐ ํ›„ s3 url์„ ๋ฐ›์•„์˜จ ํ›„…

 

5. ๋ณธ๋ฌธ์—์„œ ์•ž์„œ ์ถ”์ถœํ•œ img base url์˜ blob url์„ s3 url๋กœ ๋ณ€ํ™˜ํ•˜๊ณ 

์ตœ์ข… s3 url๋กœ ๋Œ€์ฒด๋œ img ํƒœ๊ทธ๊ฐ€ ํฌํ•จ๋œ ๋ณธ๋ฌธ์„ ์ œ์ถœํ•œ๋‹ค~ ๋„์•

// img blob string์„ s3 ์ด๋ฏธ์ง€ ๋งํฌ๋กœ replace ํ•˜๋Š” ํ•จ์ˆ˜
function replaceImgStrToS3(imgUrls: string[]) {
  let newInnerHTML = document.querySelector("#contentEditable")!.innerHTML;
  const encodedImgLst = extractImgBaseStr();
  for (let i = 0; i < imgUrls.length; i++) {
    const replaceFrom = encodedImgLst[i];
    const replaceTo = imgUrls[i];
    newInnerHTML = newInnerHTML.replace(`${replaceFrom}`, replaceTo);
  }
  return newInnerHTML;
}

 

 

๐Ÿ‘   ํ•œ ์ค„ ์š”์•ฝ

๊ฐ€๋Šฅํ•œ ๋ฐฉ๋ฒ•์„ ์ฐพ๋Š” ๊ณผ์ •์—์„œ ๋งŽ์ด ํ—ค๋งค์—ˆ์ง€ ๋Œ€๋ถ€๋ถ„์˜ ์ฝ”๋“œ๋Š” ์ง€ํ”ผํ‹ฐ๊ฐ€ ๋„์™€์คฌ๋‹ค,,,

๊ด€๋ จ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์—†๋‹ค๋Š” ๊ฒƒ๋„ ์‹ ๊ธฐํ•˜๊ณ … ๋” ๋‚˜์€ ๋ฐฉ๋ฒ•์ด ์žˆ๋Š”๋ฐ ๋ณต์žกํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ ์šฐํšŒํ•œ ๊ฑด๊ฐ€ ์‹ถ๊ธฐ๋„ ํ•˜๊ณ … 

๊ทธ๋ž˜๋„ ๋‹น์žฅ ๊ธ‰ํ•˜์‹  ๋ถ„๋“ค์€ ์•„์ด๋””์–ด๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์ง€ ์•Š์„๊นŒ ํ•ด์„œ ๊ณต์œ ํ•ด ๋ด…๋‹ˆ๋‹ค…