react-pdf와 react-markdown 함께 쓰기
react-markdown에서 ul, ol, li의 props를 뜯어보자
Table Of Contents
들어가기
react-pdf
라이브러리를 이용해서 이력서 에디터 프로젝트를 진행하고 있었다.- react로 pdf를 조작할 수 있는 라이브러리 중에서 스타일링이 제일 자유로운 라이브러리가
react-pdf
였기 때문에 사용하게 되었다. - 다른 라이브러리는 font-size정도만 조절 가능한데,
react-pdf
는 font-size, color, backgroundColor, padding, text-decofation, border 등등 생각보다 다양한 스타일링이 가능하다!
- react로 pdf를 조작할 수 있는 라이브러리 중에서 스타일링이 제일 자유로운 라이브러리가
react-markdown
라이브러리는 현재 블로그에서도 사용하고 있는 라이브러리로, 텍스트를 md 문법에 맞게 파싱해서 react component로 렌더링한다.- 전체 아키텍쳐는 여기에서 확인할 수 있다.
dangerouslySetInnerHTML
을 이용하지 않기 때문에 안전하다고 한다👍- 또한 h1, p, a, li 등 태그별로 스타일링이 편하다😊
- 이력서 에디터에서도
react-markdown
을 이용해서 내용을 렌더링하면 bullet point를 중첩으로 사용하거나, 텍스트 중간에 링크를 넣을 수 있을 것 같았다. (기존에는 그냥 텍스트로만 작동했다) - 하지만 아무도 이런 시도를 하지 않아서 혼자 삽질을 조금 하게 되었다.
components prop 넘겨주기
react-markdown 공식 Github에 쓰여 있듯, <Markdown/>
component의 components
props으로 html 태그를 어떻게 스타일링할 지 설정을 넘겨줄 수 있다.
보통 markdown을 파싱하면 a
, blockquote
, br
, code
, em
, h1
, h2
, h3
, h4
, h5
, h6
, hr
, img
, li
, ol
, p
, pre
, strong
, ul
태그가 나온다고 한다.
여기에서 급하게 필요한 것은 h1
~ h3
, li
, ul
, a
정도이기 때문에 해당 태그들만 렌더링해보자.
1. h1
~ h3
나는 MarkdownRenderer
라는 컴포넌트를 만들어서 마크다운 렌더링은 오직 이 컴포넌트에서만 신경쓰기로 했다.
전체적인 코드는 아래와 같다.
content
에는 마크다운 문법으로 된 문자열을 넘겨주면 된다.
그리고 components
prop으로 각 태그를 이름으로 하는 컴포넌트를 넘겨준다.
import { Text, StyleSheet } from "@react-pdf/renderer"; import Markdown from "react-markdown"; const FONT_SIZE = { H1: 16, H2: 14, H3: 12, }; const FONT_WEIGHT = { H1: 800, H2: 700, H3: 600, }; const markdownStyles = StyleSheet.create({ h1: { fontSize: FONT_SIZE.H1, fontWeight: FONT_WEIGHT.H1 }, h2: { fontSize: FONT_SIZE.H2, fontWeight: FONT_WEIGHT.H2 }, h3: { fontSize: FONT_SIZE.H3, fontWeight: FONT_WEIGHT.H3 }, }); export default function MarkdownRenderer({ content }) { return ( <Markdown components={{ h1(props) { return <Text style={markdownStyles.h1}>{props.children}</Text>; }, h2(props) { return <Text style={markdownStyles.h2}>{props.children}</Text>; }, h3(props) { return <Text style={markdownStyles.h3}>{props.children}</Text>; }, }} > {content} </Markdown> ); }
react-pdf
를 사용할 때의 주의점
보통의 페이지에서 h1
등의 태그를 렌더링할 때는
h1(props) { return <h1 style={markdownStyles.h1}>{props.children}</h1>; }
처럼 그냥 h1 태그를 이용해 렌더링해 주는 것이 sementic tag를 올바르게 사용하는 방법이라고 할 수 있겠다. 하지만 react-pdf
에서 h1
태그는 작동하지 않는다.
react-pdf
에서 지원하는 태그는 Document
, Page
, View
, Image
, Text
, Link
, Note
, Canvas
뿐이다. 따라서 글자를 렌더할 때에는 Text
를 이용하자.
결과
이렇게 하면 아래처럼 pdf가 렌더링되는 걸 볼 수 있다. Markdown Section아래 부분이 MarkdownRenderer
컴포넌트이다.
2. a
위에서 말했듯이 react-pdf
에서는 a
태그도 작동하지 않는다. 대신에 Link
를 써주면 된다.
...그런데 여기서 실수로 아래 코드처럼 Link가 아니라 Text 컴포넌트를 사용했는데 페이지 이동이 된다.
a(props) { return <Text style={markdownStyles.a} {...props} />; }
위와는 달리 props로 들어오는 인자들을 전부 내려줬는데 이렇게 하면 어떤 props들이 내려가는지 모르겠다. console.log를 return 위에 추가해 props에 어떤 요소들이 있는지를 출력해보자.
콘솔에는 이러한 요소들이 출력된다. 별다른 요소는 보이지 않는데, 그냥 Text
에 href
요소를 내려주면 링크가 작동하는지 궁금하다. 그래서 아래처럼 Text
에 href
props만 추가로 넘겨줬더니 작동했다.
h1(props) { return ( <Text style={markdownStyles.h1} href={"https://awesome-resume-builder.psst54.me/"} > {props.children} </Text> ); },
react-pdf 공식 문서의 Text 설명에는 href
에 관한 설명이 없는 걸로 아는데 작동한다니 신기하다🤔
아무튼 Text
대신에 Link
로 수정하고 다음으로 넘어가자.
3. ul
, li
ul(props) { <Text style={markdownStyles.ul} {...props} />; }, li(props) { <Text style={markdownStyles.li} {...props} />; },
ul
, li
도 같은 방법으로 작성했다. 이제 content를 아래와 같은 내용으로 바꾸자 pdf가 이렇게 추출되었다.
하위 리스트 1, 2가 제대로 파싱된 것 같은데, 이 둘이 왜 가로로 놓이는지 모르겠다.
1. flex-direction: column
display:'flex'
와 flexDirection:'column'
를 써서 스타일링하면 하위 리스트를 세로로 정렬할 수 있을 것 같았다.
그런데 pdf에 알 수 없는 빈 공백이 생겨버렸다. 왜 그런지 이유를 전혀 알 수 없어 props를 뜯어봐야 할 것 같다는 생각이 들었다.😭
2. props 뜯어보기
ul
과 li
모두 props를 출력했다.
로그가 총 6개 찍혔다. 조금 더 자세히 보자면
이렇다.
\n
문자가 계속 들어가 있는데, 아마도 이 문자가 렌더링되면서 빈 줄을 만들어낸 것 같다.
아무래도 children을 그대로 렌더링하게 놔두면 안 될 것 같고, children이 array인 경우, 내가 map을 직접 돌리면서 \n
문자는 출력하지 않도록 해야겠다.
array의 요소로는 string 또는 react 컴포넌트가 들어오고 있기 때문에, string인지 아닌지를 검사해서 string이라면 바로 Text
로 감싸서 return하고, 아니라면 react 컴포넌트인 것으로 간주해(사실 string이 아니면 전부 react 컴포넌트인지는 확실하지 않지만 ) child를 그대로 리턴하게 되었다.
ul(props) { // console.log("[ul props]", props); if (Array.isArray(props.children)) { return ( <div style={markdownStyles.column}> {props.children.map((child) => child === "\n" ? ( <></> ) : typeof child === "string" ? ( <Text>{child}</Text> ) : ( <div style={markdownStyles.column}>{child}</div> ) )} </div> ); } return <></>; }, li(props) { // console.log("[li props]", props); if (Array.isArray(props.children)) { return ( <div style={markdownStyles.column}> {props.children.map((child) => child === "\n" ? ( <></> ) : typeof child === "string" ? ( <Text>{child}</Text> ) : ( <div style={markdownStyles.column}>{child}</div> ) )} </div> ); } if (typeof props.children === "string") { return <Text>{props.children}</Text>; } return <></>; },
아... 이렇게까지 더럽게 쓸 생각은 없었는데 우선 이대로 진행하자.
우선 줄바꿈은 잘 된다. 이제 padding left와 bullet을 직접 추가해서 들여쓰기를 구현하면 아래처럼 된다.
3. 리스트 요소에 a
, strong
등의 태그가 섞일 경우 문제 발생
그런데 리스트 아이템 중간에 링크를 넣거나, 굵은 글씨로 강조를 할 경우 해당 요소 앞뒤로 줄바꿈이 일어나 문서가 깨지는 현상이 발생했다. 이번에도 props을 뜯어보자.
"중간에 [링크]()를 넣어보자"라는 문장을 react-markdown
이 파싱한 결과로 "중간에 "(string), "링크"(react 컴포넌트), "를 넣어보자"(string)이 반환된 것이다. 현재 코드는 li 안에 array가 있으면 해당 아이템들을 세로로 배치하게 되어 있다. (그래야 리스트 안의 리스트가 세로로 정렬되기 때문이다.)
그런데 아이템이 항상 string / react 컴포넌트(li 또는 ul)만 오는 것이 아니기 때문에 무조건 세로로 배치하면 안 된다.
복잡한 조건 분기를 어떻게 처리할까 하다가 결국 렌더링 전에 array를 조작하기로 했다.
let arr = []; let index = 0; let flag = true; props.children.forEach((child) => { if ( typeof child === "string" || child.key.includes("a") || child.key.includes("strong") ) { if (child == "\n") { } else if (flag) { arr.push({ type: "string", arr: [child] }); flag = false; } else { arr[index].arr.push(child); } } else { if (flag) { arr.push({ type: "obj", arr: [child] }); index++; } else { arr.push({ type: "obj", arr: [child] }); index++; flag = true; } } });
기본적인 아이디어는 다음과 같다.
- arr에는 list의 요소들을 담는다. (arr[0] = list의 첫 줄, arr[1] = list의 둘째 줄)
- 단, arr의 각 인덱스 역시 array이다. list의 한 줄이 단순히 string 한 줄로 이루어지지 않는 경우를 다루기 위해서이다. (["중간에 ", react 컴포넌트, "를 넣어보자])
이렇게 하면 문자열 중간에
a
,strong
태그가 들어와도 줄바꿈이 일어나지 않는다.
결과물
이제 스타일링이 잘 된다.
사담
나중에 찾아보니 react-pdf
문서에서 Text
는 다른 Text
나 요소들 안에 중첩해서 쓸 수 없다고 하는데, 일단 로컬에서는 잘 돌아갔다.
위에서 Text
에 href
를 추가해 Link
처럼 작동하게 만드는 부분도 왜 작동하는지는 잘 모르겠다.