글을 읽기 전 미리 보는 완성본은 다음과 같다.
어떻게 만들었고 적용했는지 궁금하신 분들은 아래로 스크롤!
See the Pen tistory code block by MiJeong Kim (@sap03110) on CodePen.
발단
여느 날과 다름없이 회사 맥북으로 공부하면서 블로그 글을 작성하고 있는 도중, 갑자기 내 티스토리 블로그 코드 블록 디자인이 무척이나 거슬리기 시작했다.
구글링을 하다 보면 여러 기술 블로그들을 엿보게 되는데, 대부분의 블로거 분들이 기본 코드 블록을 사용하기 보다는 디자인을 커스텀해서 사용하고 있는 것이었다.
난 참고로 기본 블로그 테마를 사용한다. 그렇다. 디자인이 민둥하다.
그 중에서도 이쁘다고 느꼈던 코드 블록 디자인은 carbon이라는 코드 이미지화 사이트에서 제공하는 mac 디자인의 코드 블록이었다.
위 사이트로 접속하면 누구든 손쉽게 이쁜 이미지 형태의 코드 블록을 만들 수 있다.
나도 한 번 만들어 봤다. 익숙하지만 이쁜 디자인을 보니 군침이 싹 돈다.
하지만 carbon은 치명적인 단점이 있었는데... 그건 바로 바로 이미지(png, svg)와 iframe으로만 export가 가능하다는 것이다.
이미지 형태의 코드 블록은 코드 드래그와 복붙이 불가능하고 iframe은 반응형 처리가 어렵다.
그리고 코드 블록이 필요할 때마다 왔다갔다 하면서 복붙을 해야 한다는 애로사항이 있다.
그래서 그냥 티스토리의 스킨 CSS를 변경하기로 했따!
각설하고, 바로 만들어서 블로그에 적용해 보자.
참고로 코드 블록 디자인은 carbon 참고, 색상 정보는 mac의 finder에서 추출했다.
디자인 개선도 하면서 실제 IDE 환경처럼 편의성 개선도 해보면 좋을 것 같아서 line index 추가, 클립보드 복사, 사용 언어 정보 등 이것 저것 추가하기로 했다.
상단바 영역 만들기
mac의 상단바처럼 코드 블록의 상단바를 추가하자.
상단바는 코드 블록의 몸통 색깔보다는 밝은 배경색이고 동그란 버튼 3개가 왼쪽에 위치한다.
스타일링이나 마크업은 어렵지 않지만, 티스토리는 코드 블록의 HTML 코드 변경 기능을 제공하지 않기 때문에 상단바를 어떻게 코드 블록 위에 그려줄 것인가에 대해 고민이 많았다.
가장 간단하게 생각할 수 있는 방법으로는 상단바를 이미지로 만들어서 CSS의 before 가상 요소의 content로 넣어주는 방법이다.
하지만 클립보드 복사 기능 구현을 위해서는 자바스크립트 사용은 불가피하기도 하고, 야매로 대충 때운다는 생각이 들어 HTML+CSS로 작성한 상단바를 자바스크립트를 이용하여 코드 블록 앞에 붙여주는 방법으로 진행하기로 했다.
작성한 상단바의 마크업과 스타일은 아래와 같다.
const codeHeader = `
<div class="code-header">
<span class="red btn"></span>
<span class="yellow btn"></span>
<span class="green btn"></span>
</div>`;
.hljs .code-header {
display: flex;
align-items: center;
padding: 14px;
background-color: #434041;
border-radius: 8px 8px 0 0;
}
.hljs .code-header .btn {
border-radius: 50%;
width: 15px;
height: 15px;
margin: 0 5px;
}
.hljs .code-header .btn.red {
background-color: #F5655B;
}
.hljs .code-header .btn.yellow {
background-color: #F6BD3B;
}
.hljs .code-header .btn.green {
background-color: #43C645;
}
현재 코드 블록의 language 정보 표시
티스토리 코드 블록의 HTML 마크업을 유심히 살펴보면 다음과 같이 pre > code > span + text 형태의 구조로 되어 있는 것을 볼 수 있다.
여기서 pre 태그의 data-ke-language 속성을 통해 우리가 코드 블록을 만들 때 선택했던 언어 종류 정보를 얻을 수 있다.
이 값을 가져와서 코드 블록에도 보여주자.
id 값이 code_로 시작하는 pre 태그가 코드 블록을 감싸는 태그이므로 해당 조건을 선택자로 갖는 가상 요소의 content 속성을 사용하여 언어 정보를 화면에 보여주게끔 처리했다.
(외부 IDE에서 작업 후 코드를 복붙해서 옮겨오는 경우에는 id 값이 지정되지 않기 때문에 스타일이 적용되지 않는 문제가 발생하여 id 구분자는 삭제했습니다.)
pre {
position: relative;
}
pre::after {
content: attr(data-ke-language);
position: absolute;
bottom: 8px;
right: 12px;
color: #cfd2d1;
font-size: 12px;
}
줄번호(라인 인덱스) 추가
줄번호를 추가하기 위해서는 pre > code 태그 내에 있는 요소들을 라인 별로 분리하고 div 태그로 감싸주는 작업을 해야 한다.
여기부터는 기존 HTML 코드 수정이 필요하다.
자바스크립트에서 pre 태그들을 긁어모은 후, pre 요소 내의 code 요소 목록을 순회하면서 HTML 값을 가져온 후 줄바꿈 문자(\n)로 슬라이싱을 해준다.
슬라이싱된 결과물(HTML과 텍스트 요소가 혼합된 라인 배열)을 블록 태그인 <div class="line></div>로 감싸주면 각각의 라인에 스타일을 적용할 준비가 끝난다.
이제 기존 코드 블록 HTML에 위에서 만든 블록 라인 배열(processedCodes)을 아까 만들었던 상단바(codeHeader)를 대입하면 변경된 마크업이 적용된다.
const codeBlocks = document.querySelectorAll('pre > code');
for (const codeBlock of codeBlocks) {
const codes = codeBlock.innerHTML.split('\n');
const processedCodes = codes.reduce((prevCodes, curCode) => prevCodes + `<div class="line">${curCode}</div>`, '');
const codeBody = `<div class="code-body">${processedCodes}</div>`;
const codeHeader = `
<div class="code-header">
<span class="red btn"></span>
<span class="yellow btn"></span>
<span class="green btn"></span>
</div>`;
codeBlock.innerHTML = codeHeader + codeBody;
}
이제 IDE처럼 줄번호를 추가하여 코드 블록의 가독성을 높여보자.
CSS에서는 동일한 선택자를 갖는 요소들에게 인덱스를 부여할 수 있는 카운터를 제공한다.
카운터에 대해 간단하게 알아보자면, 크게 다음과 같은 3가지의 기능을 제공한다.
/* 카운터를 선언하고 0으로 초기화 */
counter-reset: 카운터명;
/* 카운터 값을 1씩 증가(카운터가 선언되어 있지 않으면 먼저 선언 후 1 증가) */
counter-increment: 카운터명;
/* 현재 카운터 값을 content에 표시 */
content: counter(카운터명);
카운터의 값은 content 속성에서 사용할 수 있으며, 줄번호는 라인의 좌측에 위치한다.
따라서 before 가상 요소의 content에 카운터 값을 표출하면 된다.
다음과 같이 스타일을 작성하여 오른쪽 텍스트 정렬된 줄번호 가상 요소를 추가했다.
.hljs .line {
counter-increment: line-idx;
line-height: 1.5;
}
.hljs .line::before {
content: counter(line-idx);
width: 24px;
display: inline-block;
text-align: right;
margin-right: 16px;
font-size: 0.8rem;
color: #747A7A;
}
라인 간격 변경 + hover 트랜지션 적용
기본 코드 블록의 라인 간격은 5평 원룸처럼 협소하게 다닥다닥 붙어있기 때문에 line-height 속성을 사용해서 라인 높이를 키워줬다.
그리고 vscode의 hover 효과처럼 각 라인마다 마우스를 위로 갖다 대면 배경 색이 살짝 어두워지는 효과도 추가하자.
덤으로 라인 번호도 살짝 밝아지게끔 처리해서 몇 번째 라인 위에 마우스가 위치했는지 한 눈에 볼 수 있도록 했다.
.hljs .line:hover {
background-color: #262830;
}
.hljs .line:hover::before {
color: #cfd2d1;
}
코드 블록 최대 높이 지정 + 스크롤바 커스텀
코드 블록의 길이가 너무 길어지면 포스팅의 가독성을 해치므로 적절한 최대 높이인 600px을 지정했다.
그리고 기본으로 제공되는 스크롤바는 보다시피 굉장히 안.이.쁘.다.
그래서 나는 어두운 코드 블록 테마와 잘 어울리는 둥근 블랙 스크롤바로 스타일을 바꿔줬다.
.hljs .code-body::-webkit-scrollbar {
width: 12px;
}
.hljs .code-body::-webkit-scrollbar-thumb {
background-color: rgb(1 2 3 / 80%);
border-radius: 4px;
}
.hljs .code-body::-webkit-scrollbar-corner {
display: none;
}
스크롤바 스타일 커스텀 방법은 이전에 포스팅한 게시물을 참고하면 된다.
클립보드 복사 기능 추가
보통 유명 라이브러리 사이트의 코드 블록은 아래 사진처럼 코드를 손쉽게 바로 복사할 수 있는 copy 버튼을 제공한다.
티스토리 코드 블록에는 copy 기능이 없기 때문에 이것도 만들어 보자.
코드 복사 기능을 구현하는 방법에는 여러 가지가 있지만, 나는 비교적 최근에 나온 방법인 navigator.clipboard를 사용하여 기능을 구현했다.
navigator.clipboard는 브라우저의 클립보드에 접근할 수 있는 Web API이며, 클립보드의 내용을 읽고 쓸 수 있는 기능을 제공한다.
// 클립보드에 텍스트를 저장함
navigator.clipboard.writeText(text);
// 클립보드의 텍스트를 가져옴
navigator.clipboard.readText();
참고로 writeText와 readText는 비동기로 동작하며, Promise 객체를 반환한다.
따라서 Promise 체이닝 혹은 try ~ catch 구문을 사용한 에러 핸들링도 가능하다.
지금은 코드 복사 기능만 만들면 되므로 writeText만 사용하여 간단한 복사 함수를 정의했다.
const COPY_TEXT_CHANGE_OFFSET = 1000;
const COPY_BUTTON_TEXT_BEFORE = 'Copy';
const COPY_BUTTON_TEXT_AFTER = 'Copied';
const COPY_ERROR_MESSAGE = '코드를 복사할 수 없습니다. 다시 시도해 주세요.';
const codeWrappers = document.querySelectorAll('pre[id^=code_]');
const copyBlockCode = async (target = null) => {
if (!target) return;
try {
const code = decodeURI(target.dataset.code);
await navigator.clipboard.writeText(code);
target.textContent = COPY_BUTTON_TEXT_AFTER;
setTimeout(() => {
target.textContent = COPY_BUTTON_TEXT_BEFORE;
}, COPY_TEXT_CHANGE_OFFSET);
} catch(error) {
alert(COPY_ERROR_MESSAGE);
console.error(error);
}
}
for (const codeBlock of codeBlocks) {
const codes = codeBlock.innerHTML.split('\n');
const processedCodes = codes.reduce((prevCodes, curCode) => prevCodes + `<div class="line">${curCode}</div>`, '');
const copyButton = `<button type="button" class="copy-btn" data-code="${encodeURI(codeBlock.textContent)}" onclick="copyBlockCode(this)">${COPY_BUTTON_TEXT_BEFORE}</button>`;
const codeBody = `<div class="code-body">${processedCodes}</div>`;
const codeHeader = `
<div class="code-header">
<span class="red btn"></span>
<span class="yellow btn"></span>
<span class="green btn"></span>
${copyButton}
</div>`;
codeBlock.innerHTML = codeHeader + codeBody;
}
이제 화면에 버튼을 렌더링한 후 copyBlockCode 함수를 클릭 이벤트로 지정하고 인자로 코드 문자열을 넘겨주면 된다.
코드 문자열 내에는 여러 가지 종류의 따옴표(', ", `)가 존재할 수 있다.
따라서 템플릿 리터럴 방식으로 HTML 요소의 onclick 속성을 사용해서 함수에 문자열 인자를 넘겨줄 때는 바로 인자로 넘겨주는 것이 아니라 data-* 속성의 값으로 인코딩한 코드 문자열을 지정한 후, this 자체를 인자로 넘기는 방식을 사용했다.
완성한 코드 적용
이제 드디어 완성한 코드를 블로그에 적용하는 시간이다.
먼저 티스토리 로그인 후, 관리 - 꾸미기 - 스킨 편집 메뉴를 클릭하여 스킨 편집 페이지로 이동한다.
페이지 우측 HTML 편집 버튼을 클릭하면 다음과 같이 HTML, CSS를 편집하고 js 파일을 업로드할 수 있는 화면으로 이동할 수 있다.
건드려야 할 곳은 CSS → 파일 업로드 → HTML 메뉴 순이다.
먼저, 키보드에서 ctrl + f 단축키를 눌러서 .hljs 텍스트를 찾는다.
현재 내가 사용 중인 테마에서는 사진 기준 185번 라인에만 .hljs 관련 스타일이 존재한다.
이제 기존 스타일을 지우고 위에서 열심히 작성한 CSS 코드를 붙여넣자.
이제 다음 메뉴인 파일 업로드로 이동 후, 작성한 JS 파일을 업로드한다.
마지막으로 HTML 메뉴로 이동한 후, 업로드한 JS 파일을 import할 수 있도록 body 태그의 최하단에 다음과 같이 코드를 추가하면 끝!
<script defer src="./images/codeblock.js"></script>
트러블 슈팅(적용 이후)
기존에 블로그에 적용한 syntax highlight 플러그인과 새로 작성한 스타일이 충돌하는 바람에 코드 블록 상단바가 뒤틀리는 문제가 발생했다.
크롬 개발자 도구를 통해 display, padding 스타일이 플러그인과의 충돌로 인해 정상적으로 적용되지 않아 발생한 문제라는 것을 알 수 있었다.
이러한 경우는 보통 css 파일의 import 순서를 atom-어쩌고.min.css가 먼저 오게끔 하고 style.css가 뒤로 가게끔 수정하면 된다.
하지만 문제는 atom-one-어쩌고.min.css 파일의 import하는 link 태그는 티스토리 내부에서 동적으로 HTML에 주입되어 HTML 편집을 통해 순서를 변경할 수가 없었다.
심지어 Critical Rendering Path와 맞지 않는 위치인 body 태그 최하단에 위치해 있어서 저 위치 뒤로 style.css를 보내기도 애매한 상황이 발생...
그래서 일단 차선책으로 적용되지 않던 스타일에 !important 옵션을 추가하여 해결했다.
혹시 이 글을 읽으시는 분들 중 더 좋은 해결 방법을 알고 계신다면 댓글로 남겨주세요! 바로 반영할게요! 🥺
마무리
플러그인 스타일 충돌 문제까지 해결하고 나면 다음과 같이 정상적으로 변경된 코드 블록을 볼 수 있다.
'Project > 블로그 테마 만들기' 카테고리의 다른 글
[티스토리 블로그 테마] - 2. 티스토리 본문에 목차 추가 (12) | 2022.09.06 |
---|