프로그래밍/웹관련

[React 5] Html 문자열(string)을 React 개체로 파싱할 때 변수 및 이벤트 변환

당근천국 2022. 8. 1. 15:30

리액트에서 html을 리액트 개체로 바꾸려면 'html-react-parser'라는 패키지를 사용하면 됩니다.

이렇게 간단하면 포스팅을 할 리가 없겠죠?

 

이 패키지는 단순히 html을 리액트 개체를 바꾸는 기능만 있어서 html(혹은 문자열)에 포함된 변수는 변환하지 않습니다.

이벤트의 경우 XSS 공격위험때문에 변환을 해주지 않는다고 합니다.

 

 

1. 리터럴(Template literals) 처리

리터럴도 섞어 써도 됩니다.

(참고 : 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);

 

 

2. 변수 파싱하기

"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);

 

 

3. 이벤트 파싱하기

이제 위에서 처리한 HTML 문자열을 'html-react-parser'를 이용하여 리액트 개체로 만들어 줍니다.

이때 변환된 개체들을 바꿀 때 사용하는 것이 'replace' 이벤트입니다.

 

import parse, { domToReact } from 'html-react-parser'

 

이 예제에서는 'onclick'만 변환합니다.

다른 이벤트는 필요한 것만 각자 변환하여 사용하면 됩니다.

 

 

3-1. 자바스크립트 변환

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줄 : 버튼 태그 안의 내용을 받아 다시 생성하는 버튼에 사용합니다.

 

 

 

3-2. 리액트 함수 변환

리액트 구문으로 작성된 함수도 변환해 봅시다.

 

리액트 구문만 바꾸는 것이 아니라 자바스크립트과 구분하여 바꿔야 합니다.

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]'로 내부 함수 접근이 가능한데......

자바스크립트는 내부 함수에 어떻게 접근하는지 모르겠습니다.

(아시는 분은 댓글 남겨주세요.)

 

그래서 함수를 글로벌로 선언하고 글로벌에서 찾아서 호출하도록 구성하였습니다.

 

 

 

4. 웹팩(webpack)으로 배포(production)시 함수를 못 찾는 오류

개발(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 하면 일단 어떻게든 해볼 수 있었는데 모듈 타입은 그게 아닌가봅니다;;;;

이게 방법이 없는 건 아닐 텐데 찾지를 못하겠네요.