리액트에서 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 하면 일단 어떻게든 해볼 수 있었는데 모듈 타입은 그게 아닌가봅니다;;;;
이게 방법이 없는 건 아닐 텐데 찾지를 못하겠네요.