👀 [NextJS] 블로그 개편기 - 5. marked를 응용하여 코드블럭 디자인 개선하기
- [NextJS] 블로그 개편기 - 4. marked를 활용한 마크다운 변환기 구현하기
- [NextJS] 블로그 개편기 - 3. SCSS 입히기
- [NextJS] 블로그 개편기 - 2. Typescript 입히기
- [NextJS] 블로그 개편기 - 1. Record One
Table of Contents
개요 🔗
이전 장에서 marked를 활용하여 이 블로그만의 마크다운 변환기를 구현했다. 이 변환기를 활용하여 밋밋한 코드블럭을 좀 더 IDE 같게 개선해보자.
마크다운의 코드블럭 🔗
마크다운은 백틱(backtick)을 통해 코드블럭을 작성한다. 코드블럭의 종류는 인라인(inline) 형태와 블럭(block) 형태가 있다.
인라인형 코드블럭 🔗
인라인형 코드블럭은 백틱을 한 번 사용하여 표기하며, 아래와 같은 특징이 있다.
- 인라인 형태로 글 중간에 삽입하여 이어 쓰는 것이 가능
- 리스트, 표 등 어느 형태에서든 사용이 가능
inline code block의 예시
는 이와 같다.
블럭형 코드블럭 🔗
블럭형 코드블럭은 백틱을 세 번 사용하여 표기하며, 아래와 같은 특징이 있다.
- 블럭 형태로 온전히 한 공간을 차지함
- 공간을 차지하므로, 글 중간에 이어 쓰거나, 리스트나 표에 쓰는 것이 불가능
html
,markdown
등의 언어 지정을 지원하므로, 언어별로 디테일한 코드 표현이 가능
JAVASCRIPT
0 | const test = 'block codeblock test'; |
1 | |
2 | alert(test); |
블럭형 코드블럭의 예시는 위와 같다. GitHub와 같이 마크다운을 지원하는 사이트의 경우, 코드블럭을 통해 해당하는 언어의 하이라이팅을 지원하기도 한다.
블럭형 코드블럭 디자인 개선하기 🔗
인라인형과 코드형 중에서 블럭형 코드블럭의 디자인을 개선해보자. 블럭형의 경우 인라인형과 달리 블럭 형태로 하나의 구역을 차지하며, 코드를 표기하기 때문에 디자인할 요소가 많은 편이다.
개선할 항목은 아래와 같다.
- 기본적인 디자인 프레임 변경
- 사용된 언어(JAVA, C# 등) 표시
- 내용 복사 버튼 추가
- 라인 숫자 표시
- 라인별 색상 구분 표시
- 마우스 오버 시 해당 라인 하이라이팅 구현
순차적으로 기능을 추가해보자
기본적인 디자인 프레임 변경하기 🔗
밋밋한 형태의 코드블럭 디자인을 변경하자. 외국 블로그에서 봤었던 코드블럭 형태 중 이쁘다고 생각했던 디자인을 모티브로 만들었다.
원랜 직접 보면서 디자인하려 했는데, 막상 찾으려고 하니 나오질 않아서 기억속에 어렴풋이 남아있는 디자인을 되짚어보며 구상했다.
위와 같이 창 형태의 디자인을 취하며, 좌측 상단에 매킨토시의 창 컨텍스트가 달려있다.
이미지를 사용할 수도 있겠지만, 태그로 구현할 수 있는건 가급적 태그로 구현하거나 정 여의치 않는다면 SVG로 구현하고자 하는 편이다. 이 디자인의 경우 복잡한 문양이 없으므로 HTML 태그 단계에서 모두 구현할 수 있을 것 같다.
적용할 레이아웃은 위와 같다. 이를 HTML로 나타내면 아래와 같다.
HTML
0 | <div> |
1 | <!-- 헤더 --> |
2 | <div> |
3 | <div><!-- 적색 버튼 --></div> |
4 | <div><!-- 황색 버튼 --></div> |
5 | <div><!-- 녹색 버튼 --></div> |
6 | </div> |
7 | |
8 | <pre> |
9 | <!-- 코드 내용 --> |
10 | </pre> |
11 | </div> |
렌더러가 코드블럭을 위와 같은 디자인으로 렌더링하도록 변경하자.
marked
에서 블럭형 코드 블럭의 렌더러는 renderer.code
로 정의된다. 해당 객체의 함수를 오버라이딩하면 된다.
파라미터 | 형식 | 필수 | 내용 |
---|---|---|---|
code |
string |
Y | 코드 |
lang |
string |
N | 언어 |
형식 | 내용 |
---|---|
string |
렌더링 결과 |
renderer.code
의 파라미터와 반환값의 정의는 위 표와 같다. 위 정의에 부합하는 함수를 작성하면 된다.
TYPESCRIPT
0 | loadLanguage([ 'javascript', 'typescript', 'java', 'html', 'css', 'json', 'scss', 'sass', 'sql', 'batch', 'bash' ]); |
1 | |
2 | const renderer = new marked.Renderer(); |
3 | |
4 | // 코드블럭 렌더링 |
5 | renderer.code = (code: string, lang: string | undefined): string => |
6 | { |
7 | // 유효한 언어가 있을 경우 |
8 | if (lang && renderer?.options?.highlight) |
9 | { |
10 | code = renderer.options.highlight(code, lang as string) as string; |
11 | |
12 | const langClass = 'language-' + lang; |
13 | |
14 | return ` |
15 | <div class="codeblock"> |
16 | <div class="top"> |
17 | <div></div> |
18 | <div></div> |
19 | <div></div> |
20 | </div> |
21 | |
22 | <pre class="${langClass}"> |
23 | ${code} |
24 | </pre> |
25 | </div> |
26 | `; |
27 | } |
28 | |
29 | // 없을 경우 |
30 | else |
31 | { |
32 | lang = 'unknown'; |
33 | |
34 | const langClass = 'language-' + lang; |
35 | |
36 | return ` |
37 | <div class="codeblock"> |
38 | <div class="top"> |
39 | <div></div> |
40 | <div></div> |
41 | <div></div> |
42 | </div> |
43 | |
44 | <pre class="${langClass}"> |
45 | ${code} |
46 | </pre> |
47 | </div> |
48 | `; |
49 | } |
50 | }; |
위와 같이 함수를 오버라이딩해서 블럭형 코드 블럭의 렌더링 결과를 변경한다.
만약 언어가 같이 표기될 경우 PrismJS
로 하이라이팅을 적용하고, 언어가 미표기될 경우 텍스트 그대로 코드 블럭에 지정하는 방식이다.
각 태그가 레이아웃대로 위치하도록 스타일 코드를 지정한다.
SCSS
0 | $fd: 18px; |
1 | $fm: 14px; |
2 | |
3 | pre[class*="language-"], |
4 | code[class*="language-"] { |
5 | @include gutter; |
6 | |
7 | background-color: #161d2c; |
8 | padding: 55px 20px 20px 20px !important; |
9 | |
10 | border-radius: 10px; |
11 | |
12 | font-family: Hack, AppleSDGothicNeo, sans-serif; |
13 | color: white; |
14 | |
15 | text-align: left; |
16 | white-space: pre; |
17 | word-spacing: normal; |
18 | word-break: normal; |
19 | word-wrap: normal; |
20 | |
21 | -webkit-hyphens: none; |
22 | -moz-hyphens: none; |
23 | -ms-hyphens: none; |
24 | hyphens: none; |
25 | |
26 | overflow: auto; |
27 | } |
28 | |
29 | code:not([class*="language-"]) { |
30 | color: white; |
31 | |
32 | font-family: Hack, AppleSDGothicNeo, sans-serif; |
33 | font-size: $fd - 4px; |
34 | |
35 | display: inline-block; |
36 | |
37 | padding: 0px 4px; |
38 | margin: 0px 3px; |
39 | |
40 | border-radius: 5px; |
41 | |
42 | @media (max-width: 960px) { |
43 | font-size: $fm - 4px; |
44 | } |
45 | } |
46 | |
47 | .codeblock { |
48 | position: relative; |
49 | |
50 | .top { |
51 | position: absolute; |
52 | |
53 | top: 0px; |
54 | left: 0px; |
55 | |
56 | width: 100%; |
57 | padding: 5px 20px; |
58 | |
59 | background-color: #2b3445; |
60 | |
61 | border-top-left-radius: 10px; |
62 | border-top-right-radius: 10px; |
63 | |
64 | display: flex; |
65 | flex-direction: row; |
66 | |
67 | align-items: center; |
68 | |
69 | div { |
70 | width: 15px; |
71 | height: 15px; |
72 | |
73 | border-radius: 50%; |
74 | |
75 | margin: 0px 5px; |
76 | |
77 | &:nth-child(2) { |
78 | background-color: #fe5f57; |
79 | } |
80 | |
81 | &:nth-child(3) { |
82 | background-color: #ffbd2e; |
83 | } |
84 | |
85 | &:nth-child(4) { |
86 | background-color: #29c941; |
87 | } |
88 | } |
89 | } |
90 | } |
레이아웃에 지정된 스타일은 위와 같다.
사용된 언어 표시하기 🔗
사용된 언어를 표시해주면 사용자가 코드 블럭의 언어를 더욱 쉽게 파악할 수 있을 것이며, 작성자가 일일히 별도로 코드를 안내해주는 수고도 덜 수 있을 것이다.
블럭형 코드 블럭의 렌더러 함수인 renderer.code
에서 lang
파라미터에 사용된 언어가 할당된다. 우리는 이 파라미터를 통해 사용된 언어를 파악하고, 이를 적절히 사용할 수 있다.
위 디자인 기준으로, 헤더 부분에 언어를 표시해주는 것이 좋아보인다.
TYPESCRIPT
0 | loadLanguage([ 'javascript', 'typescript', 'java', 'html', 'css', 'json', 'scss', 'sass', 'sql', 'batch', 'bash' ]); |
1 | |
2 | const renderer = new marked.Renderer(); |
3 | |
4 | // 코드블럭 렌더링 |
5 | renderer.code = (code: string, lang: string | undefined): string => |
6 | { |
7 | // 유효한 언어가 있을 경우 |
8 | if (lang && renderer?.options?.highlight) |
9 | { |
10 | code = renderer.options.highlight(code, lang as string) as string; |
11 | |
12 | const langClass = 'language-' + lang; |
13 | |
14 | return ` |
15 | <div class="codeblock"> |
16 | <div class="top"> |
17 | <p>${lang.toUpperCase()}</p> |
18 | <div></div> |
19 | <div></div> |
20 | <div></div> |
21 | </div> |
22 | |
23 | <pre class="${langClass}"> |
24 | ${code} |
25 | </pre> |
26 | </div> |
27 | `; |
28 | } |
29 | |
30 | // 없을 경우 |
31 | else |
32 | { |
33 | lang = 'unknown'; |
34 | |
35 | const langClass = 'language-' + lang; |
36 | |
37 | return ` |
38 | <div class="codeblock"> |
39 | <div class="top"> |
40 | <p>${lang.toUpperCase()}</p> |
41 | <div></div> |
42 | <div></div> |
43 | <div></div> |
44 | </div> |
45 | |
46 | <pre class="${langClass}"> |
47 | ${code} |
48 | </pre> |
49 | </div> |
50 | `; |
51 | } |
52 | }; |
div.top
영역에 사용된 언어를 대문자로 표시하도록 구성한다.
필요하다면 디자인을 수정해줄 수도 있다. 필자의 경우 폰트 색상 정도만 변경했다.
SCSS
0 | .top { |
1 | /* top scss 생략됨 */ |
2 | |
3 | div { |
4 | width: 15px; |
5 | height: 15px; |
6 | |
7 | border-radius: 50%; |
8 | |
9 | margin: 0px 5px; |
10 | |
11 | p { |
12 | margin: 0px; |
13 | flex-grow: 1; |
14 | |
15 | color: map-get($yellow, "400"); |
16 | } |
17 | |
18 | &:nth-child(2) { |
19 | background-color: #fe5f57; |
20 | } |
21 | |
22 | &:nth-child(3) { |
23 | background-color: #ffbd2e; |
24 | } |
25 | |
26 | &:nth-child(4) { |
27 | background-color: #29c941; |
28 | } |
29 | } |
30 | } |
p
태그의 스타일을 추가한다.
내용 복사 버튼 추가하기 🔗
개발자 친화적인 사이트의 코드블럭 대부분은 코드블럭의 내용을 복사하는 버튼을 제공한다. 이를 통해 사용자는 굳이 내용 전체를 드래그하지 않고도 코드블럭의 내용을 손쉽게 복사할 수 있다.
블럭형 코드블럭 렌더링 시 버튼을 추가하고, 클릭 이벤트에 코드블럭의 내용을 복사하도록 지정하는 스크립트를 추가하면 될 것이다.
HTML
0 | <button onclick="copyCode(this);"> |
1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" data-icon="clipboard" class="i-clipboard"> |
2 | <path fill="currentColor" d="M336 64h-80c0-35.3-28.7-64-64-64s-64 28.7-64 64H48C21.5 64 0 85.5 0 112v352c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48zM192 40c13.3 0 24 10.7 24 24s-10.7 24-24 24-24-10.7-24-24 10.7-24 24-24zm144 418c0 3.3-2.7 6-6 6H54c-3.3 0-6-2.7-6-6V118c0-3.3 2.7-6 6-6h42v36c0 6.6 5.4 12 12 12h168c6.6 0 12-5.4 12-12v-36h42c3.3 0 6 2.7 6 6z"></path> |
3 | </svg> |
4 | </button> |
레이아웃은 위와 같다. 버튼 하나가 추가된다. 버튼 클릭 시, 버튼 레이아웃에 포함된 코드 블럭을 찾아 해당 내용을 클립보드에 저장하는 스크립트가 포함되어있다.
버튼의 아이콘은 SVG로 사용했다.
JAVASCRIPT
0 | /** |
1 | * 코드 복사 함수 |
2 | * |
3 | * @param {DOMElement} dom: HTML DOM |
4 | */ |
5 | function copyCode(dom) |
6 | { |
7 | window.getSelection().selectAllChildren(dom.parentElement.querySelector('pre')); |
8 | document.execCommand('copy'); |
9 | |
10 | const origin = dom.innerHTML; |
11 | dom.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" data-icon="check" class="i-check"><path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"></path></svg>'; |
12 | |
13 | setTimeout(() => dom.innerHTML = origin, 1000); |
14 | } |
코드 복사 메서드인 copyCode
는 위와 같이 구성했다. 복사 버튼 상위의 가장 가까운 pre
태그를 찾아서, 해당 내용을 복사한다. 또한 약 1초 간 버튼의 SVG를 체크 아이콘으로 변경한다.
SCSS
0 | button { |
1 | position: absolute; |
2 | |
3 | top: 50px; |
4 | right: 20px; |
5 | width: 40px; |
6 | height: 40px; |
7 | |
8 | background-color: #1e2739; |
9 | cursor: pointer; |
10 | |
11 | border: 1px solid map-get($grey, "600"); |
12 | border-radius: 10px; |
13 | |
14 | opacity: 0; |
15 | |
16 | transition: 0.5s; |
17 | |
18 | &:hover { |
19 | transition: 0.5s; |
20 | } |
21 | } |
스타일 코드는 이와 같다. 항상 고정적인 위치에 나타나도록 absolute 기반의 레이아웃을 채택했다.
이를 바탕으로 렌더러가 코드 블럭의 렌더링 과정에 추가하자.
TYPESCRIPT
0 | loadLanguage([ 'javascript', 'typescript', 'java', 'html', 'css', 'json', 'scss', 'sass', 'sql', 'batch', 'bash' ]); |
1 | |
2 | const renderer = new marked.Renderer(); |
3 | |
4 | // 코드블럭 렌더링 |
5 | renderer.code = (code: string, lang: string | undefined): string => |
6 | { |
7 | // 유효한 언어가 있을 경우 |
8 | if (lang && renderer?.options?.highlight) |
9 | { |
10 | code = renderer.options.highlight(code, lang as string) as string; |
11 | |
12 | const langClass = 'language-' + lang; |
13 | |
14 | return ` |
15 | <div class="codeblock"> |
16 | <div class="top"> |
17 | <p>${lang.toUpperCase()}</p> |
18 | <div></div> |
19 | <div></div> |
20 | <div></div> |
21 | </div> |
22 | |
23 | <button onclick="copyCode(this);"> |
24 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" data-icon="clipboard" class="i-clipboard"> |
25 | <path fill="currentColor" d="M336 64h-80c0-35.3-28.7-64-64-64s-64 28.7-64 64H48C21.5 64 0 85.5 0 112v352c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48zM192 40c13.3 0 24 10.7 24 24s-10.7 24-24 24-24-10.7-24-24 10.7-24 24-24zm144 418c0 3.3-2.7 6-6 6H54c-3.3 0-6-2.7-6-6V118c0-3.3 2.7-6 6-6h42v36c0 6.6 5.4 12 12 12h168c6.6 0 12-5.4 12-12v-36h42c3.3 0 6 2.7 6 6z"></path> |
26 | </svg> |
27 | </button> |
28 | |
29 | <pre class="${langClass}"> |
30 | ${code} |
31 | </pre> |
32 | </div> |
33 | `; |
34 | } |
35 | |
36 | // 없을 경우 |
37 | else |
38 | { |
39 | lang = 'unknown'; |
40 | |
41 | const langClass = 'language-' + lang; |
42 | |
43 | return ` |
44 | <div class="codeblock"> |
45 | <div class="top"> |
46 | <p>${lang.toUpperCase()}</p> |
47 | <div></div> |
48 | <div></div> |
49 | <div></div> |
50 | </div> |
51 | |
52 | <button onclick="copyCode(this);"> |
53 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" data-icon="clipboard" class="i-clipboard"> |
54 | <path fill="currentColor" d="M336 64h-80c0-35.3-28.7-64-64-64s-64 28.7-64 64H48C21.5 64 0 85.5 0 112v352c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48zM192 40c13.3 0 24 10.7 24 24s-10.7 24-24 24-24-10.7-24-24 10.7-24 24-24zm144 418c0 3.3-2.7 6-6 6H54c-3.3 0-6-2.7-6-6V118c0-3.3 2.7-6 6-6h42v36c0 6.6 5.4 12 12 12h168c6.6 0 12-5.4 12-12v-36h42c3.3 0 6 2.7 6 6z"></path> |
55 | </svg> |
56 | </button> |
57 | |
58 | <pre class="${langClass}"> |
59 | ${code} |
60 | </pre> |
61 | </div> |
62 | `; |
63 | } |
64 | }; |
코드블럭에 버튼이 추가된다.
라인 숫자 표시하기 🔗
간혹 코드를 설명하다보면 코드의 특정 부분을 설명해야할 경우가 생긴다. 이 경우 보통 어느 줄의 코드를 보라는 식으로 안내하지만, 코드 블럭에 라인 숫자가 표시되지 않을 경우, 사용자가 직접 코드의 줄을 찾아 확인해야하는 번거로움이 생긴다. 만약 코드가 페이지를 넘어갈 정도로 길 경우 불편함은 배로 증가하며, 이는 컨텐츠의 질 마저 하락시키는 결과로 이어진다.
사용자가 코드를 읽는데 좀 더 도움을 줄 수 있도록, 라인 숫자를 표시해보자.
HTML
0 | <table> |
1 | <tbody> |
2 | <tr> |
3 | <td><!-- 라인 넘버 --></td> |
4 | <td><!-- 코드 --></td> |
5 | </tr> |
6 | |
7 | <tr> |
8 | <td><!-- 라인 넘버 --></td> |
9 | <td><!-- 코드 --></td> |
10 | </tr> |
11 | |
12 | <tr> |
13 | <td><!-- 라인 넘버 --></td> |
14 | <td><!-- 코드 --></td> |
15 | </tr> |
16 | |
17 | <tr> |
18 | <td><!-- 라인 넘버 --></td> |
19 | <td><!-- 코드 --></td> |
20 | </tr> |
21 | </tbody> |
22 | </table> |
라인 숫자는 코드 라인과 동일한 크기를 가져야한다. 만약 조금이라도 픽셀 차이가 날 경우, 코드 라인이 많아지면 많아질 수록 이격이 발생하게 될 것이다.
이러한 차이를 맞추기 위해서, 테이블 형태의 레이아웃을 채택했다. tr
태그를 하나의 라인으로, 아래 두 td
태그를 통해 하나는 라인 숫자, 다른 하나는 코드 영역으로 분리한다. 동일한 tr
내부의 td
는 같은 줄에 위치하는 테이블의 특성을 적극 활용하면 레이아웃 CSS에 그리 많은 힘을 들이지 않아도 될 것이다.
SCSS
0 | table { |
1 | border-collapse: collapse; |
2 | |
3 | & td { |
4 | line-height: $fd + 4px; |
5 | |
6 | @media (max-width: 960px) { |
7 | line-height: $fm + 4px; |
8 | } |
9 | } |
10 | |
11 | & td:nth-child(1) { |
12 | color: #455983; |
13 | padding-right: 10px; |
14 | |
15 | border-right: 1px solid #455983; |
16 | |
17 | text-align: right; |
18 | |
19 | user-select: none; |
20 | -moz-user-select: none; |
21 | -webkit-user-select: none; |
22 | } |
23 | |
24 | & td:nth-child(2) { |
25 | width: 100%; |
26 | |
27 | padding: 0px 20px 0px 10px; |
28 | } |
29 | } |
디자인은 위와 같다. 첫 번째 td
에 숫자를 표시하고, border-right
속성을 통해 구분선을 표시한다. 이를 토대로 코드 블럭 렌더러를 변경하자.
TYPESCRIPT
0 | loadLanguage([ 'javascript', 'typescript', 'java', 'html', 'css', 'json', 'scss', 'sass', 'sql', 'batch', 'bash' ]); |
1 | |
2 | const renderer = new marked.Renderer(); |
3 | |
4 | // 코드블럭 렌더링 |
5 | renderer.code = (code: string, lang: string | undefined): string => |
6 | { |
7 | // 유효한 언어가 있을 경우 |
8 | if (lang && renderer?.options?.highlight) |
9 | { |
10 | code = renderer.options.highlight(code, lang as string) as string; |
11 | |
12 | const langClass = 'language-' + lang; |
13 | |
14 | const line = code.split('').map((item, index) => ` |
15 | <tr data-line=${index + 1}> |
16 | <td class="line-number" data-number="${index + 1}">${index + 1}</td> |
17 | <td class="line-code" data-number=${index + 1}>${item}</td> |
18 | </tr>`).join('\n').replace(/\t|\\n/, ''); |
19 | |
20 | return ` |
21 | <div class="codeblock"> |
22 | <div class="top"> |
23 | <p>${lang.toUpperCase()}</p> |
24 | <div></div> |
25 | <div></div> |
26 | <div></div> |
27 | </div> |
28 | |
29 | <button onclick="copyCode(this);"> |
30 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" data-icon="clipboard" class="i-clipboard"> |
31 | <path fill="currentColor" d="M336 64h-80c0-35.3-28.7-64-64-64s-64 28.7-64 64H48C21.5 64 0 85.5 0 112v352c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48zM192 40c13.3 0 24 10.7 24 24s-10.7 24-24 24-24-10.7-24-24 10.7-24 24-24zm144 418c0 3.3-2.7 6-6 6H54c-3.3 0-6-2.7-6-6V118c0-3.3 2.7-6 6-6h42v36c0 6.6 5.4 12 12 12h168c6.6 0 12-5.4 12-12v-36h42c3.3 0 6 2.7 6 6z"></path> |
32 | </svg> |
33 | </button> |
34 | |
35 | <pre class="${langClass}"> |
36 | <table> |
37 | <tbody>${line}</tbody> |
38 | </table> |
39 | </pre> |
40 | </div> |
41 | `; |
42 | } |
43 | |
44 | // 없을 경우 |
45 | else |
46 | { |
47 | lang = 'unknown'; |
48 | |
49 | const langClass = 'language-' + lang; |
50 | |
51 | const line = code.split('\n').map((item, index) => ` |
52 | <tr data-line=${index + 1}> |
53 | <td class="line-number" data-number="${index + 1}">${index + 1}</td> |
54 | <td class="line-code" data-number=${index + 1}>${item}</td> |
55 | </tr>`).join('\n').replace(/\t|\\n/, ''); |
56 | |
57 | return ` |
58 | <div class="codeblock"> |
59 | <div class="top"> |
60 | <p>${lang.toUpperCase()}</p> |
61 | <div></div> |
62 | <div></div> |
63 | <div></div> |
64 | </div> |
65 | |
66 | <button onclick="copyCode(this);"> |
67 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" data-icon="clipboard" class="i-clipboard"> |
68 | <path fill="currentColor" d="M336 64h-80c0-35.3-28.7-64-64-64s-64 28.7-64 64H48C21.5 64 0 85.5 0 112v352c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48zM192 40c13.3 0 24 10.7 24 24s-10.7 24-24 24-24-10.7-24-24 10.7-24 24-24zm144 418c0 3.3-2.7 6-6 6H54c-3.3 0-6-2.7-6-6V118c0-3.3 2.7-6 6-6h42v36c0 6.6 5.4 12 12 12h168c6.6 0 12-5.4 12-12v-36h42c3.3 0 6 2.7 6 6z"></path> |
69 | </svg> |
70 | </button> |
71 | |
72 | <pre class="${langClass}"> |
73 | <table> |
74 | <tbody>${line}</tbody> |
75 | </table> |
76 | </pre> |
77 | </div> |
78 | `; |
79 | } |
80 | }; |
map
을 통해 하나하나 tr
태그를 붙이므로, 이 과정에서 라인 숫자의 수를 파악할 수 있다. 첫 번째 td
에 map
의 인덱스 index
가 지정되도록 구성했다. 태그의 의미론적인 측면을 강화하기 위해서 각 tr
과 td
의 data-number
속성으로 인덱스를 같이 지정한다. 인덱스의 시작이 0임에 주의하자.
예를 들어, 126번 째 줄은 아래와 같이 렌더링될 것이다.
HTML
0 | <tr data-number="126"> |
1 | <td data-number="126">126</td> |
2 | <td data-number="126"><!-- code --></td> |
3 | </tr> |
라인별 색상 구분하기 🔗
인터넷에서 빼곡히 적힌 글을 읽다보면 가끔 읽던 글의 위치를 헷갈리기도 한다. 특히 코드의 경우 그 특성 상 문장의 의미가 매우 옅으며 복잡하기 때문에 헷갈리는 정도가 더욱 심해진다.
이러한 피로를 줄이기 위하여 라인별로 색상을 다르게 칠해주면 읽는 과정의 피로도를 줄일 수 있을 것이다. 홀수와 짝수 라인의 색상을 서로 다르게 지정하는 전통적인 방법을 사용할 것이다.
다행히도, 우리는 라인 숫자를 표시하기 위해 하나의 라인을 tr
태그로 관리하고 있다. 즉, 홀수 tr
과 짝수 tr
의 색상을 다르게 지정해주면 된다. CSS
의 nth-child
선택자를 사용하면 매우 간단하게 해결할 수 있을 것이다.
SCSS
0 | table { |
1 | & tr:nth-child(2n) { |
2 | background-color: #1c2335; |
3 | } |
4 | } |
색상은 기존의 코드 블럭의 디자인에 적절히 어울릴 수 있도록, 기존의 배경색에서 살짝 옅은 색을 지정했다. 짝수 라인의 색상이 살짝 옅어지도록 지정될 것이다.
마우스 오버 시 해당 라인 하이라이팅하기 🔗
코드 블럭에 상호작용 기능을 하나 더 추가해보자. 코드 라인에 마우스를 올렸을 때, 해당 라인에 하이라이팅이 되도록 구현한다. 사용자는 코드 블럭에 마우스를 올림으로써 자신이 읽고 있는 코드의 가독성을 증폭시킬 수 있으며, 코드 블럭과의 상호작용을 통해 컨텐츠의 흥미 또한 이끌어낼 수 있을 것이다.
위와 마찬가지로, :hover
선택자를 통해 순수 CSS
영역에서 해결할 수 있다.
SCSS
0 | table { |
1 | & tr:hover { |
2 | background-color: #546687; |
3 | |
4 | & td:first-child { |
5 | color: white; |
6 | } |
7 | } |
8 | } |
이 정도면 될 것이다. tr
태그에 호버링을 할 경우, 해당 라인의 배경색과 라인 숫자를 밝은 색으로 변경한다. transition
속성을 주어 부드러운 시각효과를 기대할 수 있을 것이다.
정리 🔗
블로그 개편 과정에서 코드 블럭은 특히 신경을 많이 쓴 부분이였다. 디자인과 기능이 생각한대로 잘 뽑혀줘서 다행이다. marked
를 적용한 이후로 렌더링 과정을 마음대로 커스터미이징할 수 있는 점을 적극 활용했다.
다음은 수식을 표현하는 LaTeX를 적용해보자.
📆 작성일
2021-11-07 Sun 21:13:57
📚 카테고리
🏷️ 태그