과제하느라 시간을 다보냈다..그리 어려운개념도 아니었고 했는데 ..
오늘은 웹에디터라는 것을 배웠다.
웹에디터가 무슨 폰트를 꾸며주고 한다는것이라는데 진짜 처음에는 이게 뭔가 했다.
웹에디터: 글을 작성시에 글씨를 꾸며주는 기능을 제공하는것. 지금 사용하는 벨로그에도 적용되어있는데, 폰트 사이즈나, 코드 첨부, 이미지, 링크, 굵기, 기울기, 밑줄 등이 제공된다
유명한 라이브러리로는 세가지가 있는데 먼저
- React-draft-wysiwyg :얘는 React-quill보다 디자인부분에서 괜찮은것 같아 알아보니 페이스북에서 만든것 같다. 다만 이미지를 등록하고 한글을 적으면 에러가 뜬다고 한다.
2.React-quill: 실습하며 사용해본 것이다.
오류없이 무난하게 사용한다는 말도 있고, 어디서는 한글을 작성하는데 일정부분이 잘려서 나온다는 말이 있지만 일단은 문제는 없었다.
3.Toast-UI-Editor: 국내에서 만들어 다운로드 수는 적지만 노션처럼 사용하는 에디터이다.
yarn add react-quill
보니까 전용 css도 있다.
import "react-quill/dist/quill.snow.css";
이부분을 import 해주지 않는다면 굉~장히 이상한 내가 추가해주지도 않은 아이콘들이 뜬다
꼭 ant-design과 유사하다
얼마나 당황했던지...
꼭 라이브러리등을 사용할때는 해당 css가 있는지 봐주는것이 좋다.
그다음 해당부분에 태그로 ReactQuill이라고 적어준다.
ant-design과 유사하다하니 스타일부분도 유사하지 않을까싶어 가로세로를 지정해주어봤다.
import ReactQuill from "react-quill"; import "react-quill/dist/quill.snow.css"; import dynamic from "next/dynamic"; import styled from "@emotion/styled"; export default function WebEditorPage() { // ReactQuill 만든 개발자들이 만든 onChange이므로 event대신 value가 들어옴(이벤트 안들어옴) const onChangeContents = (value: string) => { console.log(value); }; const ReactQuillTextarea = styled(ReactQuill)` width: 900px; height: 450px; `; const onClickSubmit = async () => { const { Modal } = await import("antd"); // 코드 스플릿팅!(code-splitting) Modal.success({ content: "성공" }); }; return ( <div> 작성자: <input type="text" /> <br /> 비밀번호: <input type="password" /> <br /> 제목: <input type="text" /> <br /> 내용: <ReactQuillTextarea onChange={onChangeContents} /> <br /> <button onClick={onClickSubmit}>등록하기</button> </div> ); }
이런식으로 import해서 넘겨주고 해당태그에 css를 적용해주었다. ant-design에서 css를 적용하는것과 동일한 방법으로
const Style이름 = styled.태그명`
적용할 스타일
`
이런식으로 적어준부분에 .태그명대신 소괄호 안에 import해온 해당태그명을 적어주면 되었다.
ReactQuill의 onChange ---> 우리가 이제껏 사용한 onChange와는 다르다.
이때의 onChange는 리엑트 개발자가 만든 onChange라 이벤트가 아니라 입력한 값이 value같은 이름으로 들어온다.
그대로 실행해 보았다.
document is not defined라는 에러가 발생하였다.
이전에 window is not defined, localStorage is not defined 등과 같은 형태다.
해당 에러는 react-quill에서 document에서 접근해 생기는 오류인것으로 보인다고 하셨다.
==> >
만약 같은 오류가 생겼는데 못찾겠을경우. 하나하나주석처리하면서 찾아보는것이 좋다고 한다; 오늘 도움주신분이 알려주셨다.
여튼, 해당오류는 프론트엔드서버라고 불리는 Webpack Server에서 프리랜더링 되며 발생하는 오류다. 즉, 윈도우 기능인데 프리랜더링시 찾지 못해 발생하는것이다.
브라우저에서만 보여줘 설정하기
useEffect, typeof window, process.browser 들중 하나를 사용해 설정하기.
process.browser 를 사용해 브라우저일때만 보여달라고 설정하기
{process.browser && <ReactQuillTextarea onChange={onChangeContents} />}
그런데 이렇게 적용해도 해결되지 않는다.
==> 가끔씩 이렇게 import 자체가 안되는 라이브러리들이 있다고한다.
==>다르게 접근하기
아예 브라우저에서만 import되게 만들기
동적으로 import한다고 하여 동적 import = dynamic import 라고 한다.
import dynamic from 'next/dynamic';
리엑트와 넥스트의 큰 차이는 리엑트는 프리랜더링 과정이 없어 리엑트에서 import 하는것은 에러 발생이 없고, 넥스트에서는 프리랜더링때문에 해당 에러가 발생한다.
next는 프리랜더링을 한번한다. 이때 react-quill에대한 import 가 막혀있기에(안되기에)해당에러 발생.
기존의
import ReactQuill from "react-quill";
이부분 대신에 변수로 담아 다이나믹을 적용해준다.
const ReactQuill = dynamic(async () => await import("react-quill"), {
ssr: false,
});
react-quill얘를 import 해줘 ~ 근데, ssr(서버에서 랜더링은 ) false( 안할거야~!)
이렇게만 해주면 아예 브라우저가 아닐때는 import 되지 않으니 브라우저일때만 실행해줘! 라고해서 proccess.browser를 사용했던 부분은 필요가 없어진다.
함수는 처음에 페이지 접속하자마자 실행되는게 아니라 접속시 선언만되고, 실제 이벤트 발생시 실행된다.따라서 특정 함수실행시 발생하는 모달을 미리 import 하는것은 전체적인 속도를 늦출 수 있어 별로 좋은 방법은 아니다.
이것들을 다이나믹 import를 사용해 적어줄 수 있다.
기존애 상단에 import 했던 부분을 함수 안으로 옮기면된다.
Modal부분으로 실습
그런데 이 Modal의 경우에는 리엑트에서 사용하는 다이나믹 라우팅을 이용한다. 여기서는 클릭해야 실행되는 다이나믹import라고 부른다.
const onClickSubmit = async () => { const {Modal} = await import ("antd") Modal.success({content:"등록성공"}) }
import dynamic from 'next/dynamic' 을 사용하는 이유
:페이지를 접속하자마자 실행되는데 next는 리엑트와 다르게 프리랜더링 과정이 있어 서버에서 프리랜더링되는 것을 막기위함
const {Modal} = await import ("antd")
이 부분을 객체로 하나 뽑아오는것이 아니라 변수로 해서 뽑는 방법도 있다
const QQQ = await import ('antd')
QQQ.Modal.success({content:"등록성공"})
이런식으로 변수에서 뽑아 쓰는 것이다.
이렇게 따운로드를 나누어놓아 랜더링 속도를 빠르게 한것을 코드를 분리했다라는 의미로
코드스플릿
이라고한다.
리엑트 훅 폼을 이용해 감싸보기
이번에는 form을 이용해보았다. useForm을 import해서 onChange등의 기능이 들어있는 register, 그리고 버튼클릭시에 저장된 정보들을 보내주는 handleSubmit을 import받는다.
그런데 다른것들은 register에서 각각의 해당키를 받아오면되는데, 이 react-quill이라는 것은 좀 다르다. register에도 onChnge기능이 있고, 퀼에도 해당기능이있는데 둘은 엄연히 다른 것이기에 사용할 수 없는것. 따라서 우리가 알던 onChange가 아니라 event가 들어가지않는다. 그럼 변경된값을 어떻게 저장할 수 있을까?
강제로 값을 할당해주는 방법이 있다.
이럴경우에 사용하라는 from에서 제공하는 기능이 있다.
setValue라는 것을 import 해서 사용할 수 있다.
그리고 onChange하여 값이 바뀌었는지 알려주는 trigger 라는 기능도 import 하여 사용하면 기존에처럼 사용이 가능하다.
이번에는 mutation을 날려보았다.
그랬더니
이렇게 태그가 다 화면에 노출되는것을 볼 수 있다.
우리의 최종목적은 저 태그들이 눈에 안보이고태그가 적용된것이 보이도록 하는것이다.
따라서 html에 해당부분이 직접삽입되게 하는 방법을 사용한다.
받아오는 부분에 html코드라고 다이렉트로 입력해준다
원래 내용을 받아오는 태그에 안쪽에 dengerouslySetHTML을 사용해 해당부분(내용)을 강제로 넣어준다.
이렇게 사용하면 태그 자체로 바로 실행되기에 quill의 스타일을 적용해준대로 나오게된다.
그런데 문제가 발생한다. dengerouslySetHTML 이 이름을 보니 dengerously라고 앞에 적혀있다.
보안상 문제가 있을수도 있다는것을 알고도 사용하려하나? 라는 의미로 알면 되겠다.
그럼 어떤 보안의 문제가 있나?
다른태그로 위장해 중요한 토큰들의 정보들을 빼갈수 있다는 문제가 있다.
그럼 어떤식으로 빼가게될까?
일단 게시물 등록시 script태그 등을 사용해 원하는 정보를 그 태그에 적어 자신의 벡엔드 API로 보내는 코드를 적어 누군가 그 게시물에 접근한다면 그 사람의 정보가 탈취되는 식이다.
줄여서 ### XSS(Cross Site Script)
그런데 이 방법으로 탈취하는것은 react-quill에서 막아주고 있다.
실제로 태그를 직접 작성해 등록하면 그 태그들은 눈에서 안보이지도 않고, 콘솔창을 확인해보면 태그들 사이에 < , >등으로 들어가게된다.
1단계로 막아주는 기능을 해주는것이다.
그렇다고 해커들이 정보를 가져가는 방법이 없는건 아니다. 프론트로 등록이 안되면 위치를 바꾼다.
바로 벡엔드서버를 해킹해 벡엔드로 직접들어가 해당태그가 들어있는 게시물을 등록하는것이다.
보통 script태그는 목적이 너무 뻔한 태그이기에 자동적으로 막아지니 img등의 태그를 사용한다. 원래 src부분에 가서 이미지를 받아와야하지만, 이부분을 없는 주소로 작성하고, 실패시에 자신이 원하는 동작을 하도록 만든다.
아래가 예시이다.
<img src="#" onerror="console.log(localStorage.getItem('\accessToken\'))" />
이렇게 되면 일단 이미지를 가져오려 시도하나 에러가 나니 onerror부분으로 넘어가 정보가 탈취되는 방식이다.
따라서 꺽쇠가 있는 것이 있으면 벡엔드에서 막아주는 작업이 필요하다.
그렇다고 프론트에서는 이대로 있어야하는게 아니다. 방어는 해도해도 끝이 없으니 프론트에서도 막아주는 작업이 또 필요하다.
Dompurify라는 라이브러리를 사용해보았다
yarn add dompufify
yarn add @types/dompurify --dev
해당라이브러리와 해당되는 타입을 설치한다.
html태그를 가져올때 앞에 Dompurify,sanitize를 붙여준다.
{typeof window !== "undefined" && ( <div dangerouslySetInnerHTML={{ __html: Dompurify.sanitize(data?.fetchBoard.contents), }} </div> )}
이렇게 __html 부분 실제 태그가 들어오는 부분앞에 붙여주어 등록시에 한번 검사하게 만든다.
그래서 보여줄때도 태그가 들어오지 못하게 막아준다.
그런데 dompurify도 프리랜더링 문제때문에 로직상 문제가 없음에도 에러가 나며 페이지가 보이지 않는다/
따라서 브라우저에만 보이도록 설정하는 부분이 필요하다
typeof window 가 undefined가 아닐때 즉 윈도우타입일때만 실행되도록 앞에 붙여주도록 한다.
이렇게 Dompurify등을 위해 검증해주는 보안코딩을 시큐어코딩 이라고한다.
react-quill을 이용해 글을 작성시에 value를 콘솔창에 찍어보니 각 문단은 <p>
태그로 감싸져 있는것을 알 수 있다.
그런데 내용을 다 지웠을때에도
<p><br/><p>
이 태그가 남아있는것을 확인할 수 있었다.
따라서 삼항연산자를 활용해 만약 value가 저 태그라면 빈 값만 보내고, 아니면 value가 들어가도록해준다.
setValue("contents", value === "<p><br/></p>" ? "" : value);
// import ReactQuill from "react-quill"; import "react-quill/dist/quill.snow.css"; import dynamic from "next/dynamic"; import styled from "@emotion/styled"; import { useForm } from "react-hook-form"; const ReactQuill = dynamic(async () => await import("react-quill"), { ssr: false, }); export default function WebEditorPage() { const { register, handleSubmit, setValue, trigger } = useForm({ mode: "onChange", // 변경시마다 검증.(비제어 컴포넌트 방식을 제어컴포넌트 방식으로. 즉 입력시마다 검증되게) }); // const [value, setValue] = useState(); // const qqq = (값) => setValue(값); // ReactQuill 만든 개발자들이 만든 onChange이므로 event대신 value가 들어옴(이벤트 안들어옴) const onChangeContents = (value: string) => { console.log(value); // 콘솔에 찍히는 것을 확인해보니 입력된내용들이 <p>태그로 감싸져 들어감. 그런데 내용을 다 지웠더라도 "<p><br/><p>"태그가 남아있는것을 콘솔창에서 확인. 이것도 지워주기위해 조건담 setValue("contents", value === "<p><br/></p>" ? "" : value); // register로 등록하지 않고 강제로 값을 넣어주는 기능//onChange모드로 바꾸기 트리거 해주는 기능추가(검증위함) void trigger("contents"); // onChange됐다고 강제로 알려주는 기능(위에서 엄급한것처럼. 검증위함)) }; const ReactQuillTextarea = styled(ReactQuill)` width: 900px; height: 450px; `; return ( <div> 작성자: <input type="text" {...register("writer")} /> <br /> 비밀번호: <input type="password" {...register("password")} /> <br /> 제목: <input type="text" {...register("title")} /> <br /> 내용: <ReactQuillTextarea onChange={onChangeContents} /> {/* onChange가 리엑트 훅폼의 register에도 있고, 리텍트 퀼에도 있어 겹침. 따라서 여기에는 register못 넣고 수동으로 넣어주기...? setValue 라는 것이 제공됨. 입력하면 onChangeContents가 작동,value가 들어가게됨. 그 value를 setValue에 넣어주기 다만 value에 키값을 적어주고... setValue("키",value) */} <br /> <button>등록하기</button> </div> ); }
지금까지의 내용정리:
react-quill적용하며 다이나믹 import 하여 프리랜더링 문제 해결.,
등록하기 후 상세페이지에서 퀼을 적용해 글을 등록하였더니 태그가 그대로 노출되는것이 보였는데 이부분을 dengerouslySetInnerHTML을 이용해 태그를 html에서 실행되게 만들어줌.(주의해야할것. div태그안이 아니라 div태그에 작성해줄것.).,
보안검증위해 dompurify를 사용(정보탈취 방지 즉, XSS크로스 사이드 스크립팅 방지 목적)
유명 보안 문제들 :owasp top 10 이라는 것이 3~4년에 한번씩 나오고 있음.
가장 자주생기는 보안문제들을 정리해놓은것.
크로스 사이드 스크립트와 함께 따라다니는 크로스 사이트 리퀘스트 라는 문제도 보이는데 이부분은 탈취한정보를 사용하는 부분인것 같다.
Injection =>
인젝션이라는 보안문제를 강조해주셨는데 매번 등장하는 문제이고 비록 벡엔드에서 발생하나, 프론트엔드도 알고있어야할 개념이다.
간단히 말해 연산자를 이용해 정보를 탈취하는 방식같았다.
로그인을 예로 들어보자
일단 해커가 아이디는 알고있는 상황이다. 그러면 로그인시 id가 해당 아이디일경우에 비밀번호만 맞으면 로그인이 된다. 둘은 if문의 조건이다
그런데 이 경우에 비밀번호에 먼저아무거나 입력을 하고 뒤에 || 이 or연산자로 앞에게 틀리면 true로
비밀번호: 아무거나 ||true
결국에 true로 accessToken이 발급되는 식으로 정보를 탈취하는 방식이다.
이것을 SQL인젝션이라고하고 벡엔드에서 막아줄 필요가있다.
지금껏 윈도우일 경우에만 실행하기로한 태그들이 있었다
각각에 스타일을 적용해보았다
<div style={{ color: "red" }}>작성자:{data?.fetchBoard.writer}</div>
<div style={{ color: "blue" }}>제목:{data?.fetchBoard.title}</div>
{/* <div>내용:{data?.fetchBoard.contents}</div> */}
{typeof window !== "undefined" ? (
<div
style={{ color: "green" }}
dangerouslySetInnerHTML={{
__html: Dompurify.sanitize(data?.fetchBoard.contents),
}}
></div>
)}
<div style={{ color: "brown" }}>주소: 구로구</div>
이렇게 스타일을 주게되면 색이 제대로 적용되지 않고 어떤색은 두번 나오고 하는 이상한 현상이 발생 하였다.
이유: 서버에서 프리랜더링했을때의 태그와 브라우저에서 그릴때의 결과가 다르기때문.
다시말해 지금 브라우저에서만 그려달라는 태그가 있어서 서버에서 그린 내용을 화면에 덮어쓰기에 사라지게되는것.
따라서 삼항연산자로 태그개수를 맞춰주어 해결할 수 있다.
이때 적용할 스타일도 동일하게!
<div style={{ color: "green" }} dangerouslySetInnerHTML={{ __html: Dompurify.sanitize(data?.fetchBoard.contents), }} </div> ) : ( <div style={{ color: "green" }}></div> )}