리액트에서 html을 리액트 개체로 바꾸려면 'html-react-parser'라는 패키지를 사용하면 됩니다.
이렇게 간단하면 포스팅을 할 리가 없겠죠?
이 패키지는 단순히 html을 리액트 개체를 바꾸는 기능만 있어서 html(혹은 문자열)에 포함된 변수는 변환하지 않습니다.
이벤트의 경우 XSS 공격위험때문에 변환을 해주지 않는다고 합니다.
리터럴도 섞어 써도 됩니다.
(참고 : Mdn Docs - Template literals)
그러려면 순서가
리터럴 처리 -> 리액트 변수 처리 -> 이벤트 처리
로 해야 합니다.
자바스크립트 안에서 단순 문자열을 선언하는 경우라면 그레이브(`)를 사용해도 됩니다.
하지만 'html-react-parser'쓸 정도라면 템플릿으로 사용하는 html 파일이 별도 있을 확률이 높죠.
리액트 랜더에서 쓰는 코드 모양과 많이 달라진다는 문제도 있습니다.
문자열로 저장된 HTML을 리터럴 처리하려면 별도의 함수가 필요합니다.
참고 : stackoverflow - 'Convert a string to a template string'의 'Mateusz Moska'님 답변
/**
* 문자열을 리러털로 변환
* https://stackoverflow.com/a/41015840/6725889
* @param {json} params 데이터로 사용할 json
*/
String.prototype.interpolate = function (params)
{
const names = Object.keys(params);
const vals = Object.values(params);
return new Function(...names, `return \`${this}\`;`)(...vals);
}
//리터럴 문자 변환
reactsTest3 = sTest3.interpolate(jsonData);
"html-react-parser"로 파싱하기 전에 문자열 안에 있는 리액트 변수를 찾아서 리플레이스해 줘야 합니다.
이때 일치하지 않는 리액트 변수는 처리하지 말고 둡니다.
리액트는 변수를 중괄호({, })로 감싸므로 중괄호를 찾아 변환합니다.
/**
* 문자열에서 리액트 문법의 변수를 찾아 변환하여 리턴한다.
* @param {any} jsonParams 찾을 변수명: 데이터
*/
String.prototype.replaceReact = function (jsonParams)
{
let sReturn = this;
//처리할 대상
let arrTarget = sReturn.match(/\{[\w]+\}/g);
arrTarget && arrTarget.forEach((jsonItem) =>
{
let regex = new RegExp(jsonItem, 'g');
let stateItem = jsonItem.split(/{|}/g)[1];
let objTarget = jsonParams[stateItem];
if (objTarget)
{//대상이 있다.
sReturn = sReturn.replace(regex, objTarget);
}
//대상이 아니면 그냥 둔다.
});
return sReturn;
}
//리액트 변수 변환
reactsTest3 = reactsTest3.replaceReact(jsonData);
이제 위에서 처리한 HTML 문자열을 'html-react-parser'를 이용하여 리액트 개체로 만들어 줍니다.
이때 변환된 개체들을 바꿀 때 사용하는 것이 'replace' 이벤트입니다.
import parse, { domToReact } from 'html-react-parser'
이 예제에서는 'onclick'만 변환합니다.
다른 이벤트는 필요한 것만 각자 변환하여 사용하면 됩니다.
html의 이벤트에는 자바스크립트가 입력될 수 있으므로 자바스크립트 부터처리해 봅시다.
parse(sTest2
, {
replace: domNode =>
{
if (domNode.name === 'button')
{//버튼 이벤트 처리
let sFuncName = domNode.attribs.onclick;
delete domNode.attribs.onclick;
return (
<button
{...domNode.attribs}
onClick={() => { Function('"use strict";return (' + sFuncName + ')')(); }}
>{domToReact(domNode.children, {})}</button>
);
}
}
});
여기서
7줄 : 이 개체의 onclick의 내용을 받습니다.
9줄 : 기존 클릭 이벤트를 제거하고
11줄 : 새로 버튼을 렌더링합니다.
13줄 : 속성을 기존에 있는 것을 그대로 사용합니다.
14줄 : 함수 내용을 실행시킵니다.
- 이 코드는 자바스크립트를 격리한 후 함수 이름으로 실행시키는 코드입니다.
15줄 : 버튼 태그 안의 내용을 받아 다시 생성하는 버튼에 사용합니다.
리액트 구문으로 작성된 함수도 변환해 봅시다.
리액트 구문만 바꾸는 것이 아니라 자바스크립트과 구분하여 바꿔야 합니다.
parse(sTest2
, {
replace: domNode =>
{
if (domNode.name === 'button')
{
console.log(domNode);
let temp = domNode.attribs.onclick;
//기본 빈 함수
let funcCall = function (event, param) { };
//기존 로드의 클릭이벤트 제거
delete domNode.attribs.onclick;
if ("{" === temp.substring(0, 1)
&& "}" === temp.substring(temp.length - 1))
{//앞뒤로 있는게 중괄호다 = 리액트 함수
//리액트 함수로 취급한다.
temp = temp.split(/{|}/g)[1];
//클래스일때
//funcCall = this[temp];
//자바스크립트일때
funcCall = window[temp];
}
else
{//자바스크립트
funcCall = function (event, param)
{
Function('"use strict";return (' + temp + ')')(event, param);
};
}
return (
<button
{...domNode.attribs}
onClick=
{(event, param) =>
{
funcCall(event, param);
}}
>{domToReact(domNode.children, {})}</button>
);
}
}
});
21~24줄 : 클래스에서 사용하는 경우 'this[temp]'로 내부 함수 접근이 가능한데......
자바스크립트는 내부 함수에 어떻게 접근하는지 모르겠습니다.
(아시는 분은 댓글 남겨주세요.)
그래서 함수를 글로벌로 선언하고 글로벌에서 찾아서 호출하도록 구성하였습니다.
개발(development) 빌드에서는 문제가 없는데 배포(production) 빌드에서 함수를 찾지 못하는 오류가 날 수 있습니다.
Uncaught ReferenceError: [함수명] is not defined
오류를 추적해보면 미니마이즈(minimize)된 HTML이 이상한 걸 확인 할 수 있습니다.
이것은 'html-loader'가 읽은 'HTML' 파일 안에 있는 내용을 축소하면서
필요 없는 구문인 중괄호({, })를 제거하기 때문에 발생합니다.
'React'구문에서 중괄호로 함수나 변수를 감싸는데 이걸 제거하니 우리가 새로 만든 파서가 인식을 못 하는 것이죠.
해결 방법은 'html-loader'옵션의 미니마이즈 옵션에서 자바스크립트를 제외하면 됩니다.
{//html 파일
test: /\.html$/i,
loader: "html-loader",
options: {
minimize: {
minifyJS: false,
},
},
},
샘플 프로젝트 : github - dang-gun/HtmlJavascriptSamples/ReactHtmlParsing/
프론트엔드도 빌드하니까 여러 가지로 편하고 좋기는 한데....
예상대로 접근할 수 있는 스코프 찾는 게 헬이네요.
자기 개체를 못 찾아서 한참 헤매다가 포기하고 그냥 포스팅했습니다.
원래 모던에서는 this 하면 일단 어떻게든 해볼 수 있었는데 모듈 타입은 그게 아닌가봅니다;;;;
이게 방법이 없는 건 아닐 텐데 찾지를 못하겠네요.