ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • ๊ธ€ ์‚ฌ์ด ์ด๋ฏธ์ง€ ๋„ฃ๊ณ  ๊ด€๋ฆฌํ•˜๊ธฐ
    ๐Ÿ‘ 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;
    }
    

     

     

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

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

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

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

     

     

     

    '๐Ÿ‘ FE' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

    ์‚ฌ๋‚ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ฐœ๋ฐœ ๊ธฐ๋ก (feat. monorepo, submodule)  (0) 2024.04.09
Designed by Tistory.