
블로그로 코드를 공유하고 바로 그 페이지에서 바로 에디터 확인하여 코드 수정하고 실시간으로 반영되어 볼 수 있게 뷰어도 만들어보자.
처음엔 iframe 만들어서 썼는데 매일 iframe 만들어서 공유하는 것은 정말 쉽지 않은 것 같더라….
온라인 에디터 & 뷰어
jsdelivr 나 codepen 같은 사이트에서 제공하는 것처럼 최초에 저장되었던 코드가 나오고 그 코드를 수정할 수 있는 에디터 + 실시간 결과가 나오는 뷰어를 만들고 싶다.
어떻게 해야 코드를 수정 가능하게 공유하고 실시간으로 코드를 적용해서 보여줄 수 있을지 방법을 알아보자.
먼저 어떤 기능으로 만들 것인지 정리해보자.
- 구문 강조(Syntax Highlighter)를 위해 이용한 <code></code> 안에 들어가 있는 코드를 활용하자.
- 코드를 에디터에 모아 놓고 수정할 수 있어야 한다.
- 에디터에 들어간 코드는 자동으로 iframe에서 열 수 있게 하자.
- 내가 에디터에 들어간 코드를 수정하면 iframe에 바로 반영되게 하자.
- 에디터와 뷰어는 항상 같이 사용하자.
- 뷰어는 모바일, 태블릿, 컴퓨터 버튼을 만들어 놓고 각각 기기를 누르면 그 각각의 기기에선 어떻게 보이는지 대략적으로 확인할 수 있다.
- 모바일에서는 다른 기기를 볼 필요가 없으므로 기기 선택하는 메뉴가 안 나오게 하고 태블릿에서도 컴퓨터의 화면을 볼 순 없으니 컴퓨터 아이콘이 보이지 않게 한다.
- iframe, textarea에 border:0; 이어도 작동이 잘 되어야한다.
- iframe의 높이는 내부 높이를 확인하여 자동 조절 되어야한다. 그런데 border가 생기면 스크롤바가 무조건 생기니까 border 없이 무조건 작동되어야한다.
- iframe 높이가 커졌다가 다시 작아질 때 딜레이가 없어야한다.
온라인 에디터 & 뷰어 만드는 방법
에디터는 수정할 수 있어야 하므로 <code>를 그대로 활용하진 못한다.
그래서 고민하다가 textarea로 만들기로 결정했다.
HTML
HTML 코드를 입력할 때 <pre><code class="language-markup"> … </code></pre> 사이에 있는 < > 등은 반드시 HTML 엔티티 코드로 넣어야 한다.
HTML entity encoder/decoder 페이지에 있는 텍스트 필드 하단에 only encode unsafe and non-ASCII characters, allow named character references in output (incompatible with older browsers) 두 가지 항목을 체크하면 좀 더 편하게 쓸 수 있다.
아래 코드를 참고하면 어렵지 않게 이해할 수 있다.
<pre><code class="language-markup"><h2>안녕하세요.</h2>
<p>환영합니다.</p>
<p id="date"></p></code></pre>
<pre><code class="language-css">@charset "utf-8";
html,body{
background-color:#fff;
color:#000;
}
h2
{
font-size:3rem;
color:#555;
font-weight:500;
}
@media (prefers-color-scheme:dark) {
html,body{
background-color:#000;
color:#fff;
}
}</code></pre>
<pre><code class="language-js">var d = new Date();
document.getElementById("date").innerHTML = d;</code></pre>
<div class="code-container">
<div class="editor code-wrap">
<textarea id="code-editor" name="editor" placeholder="HTML, CSS, JavaScript 코드 입력하면 아래 VIEWER에서 실행됩니다."></textarea>
</div>
<div class="preview">
<div class="device-controls">
<button type="button" class="device-btn" data-device="mobile"></button>
<button type="button" class="device-btn" data-device="tablet"></button>
<button type="button" class="device-btn" data-device="laptop"></button>
</div>
<div class="browser-frame code-wrap">
<iframe class="code-viewer" loading="lazy"></iframe>
</div>
</div>
</div>
CSS
@charset "utf-8";:root{--device-color:#b0b0b0;--device-active-color:#8f8f8f;--device-border-color:#f5f5f5;--device-accent-color:#888}#code-editor,.code-viewer{width:100%;padding:0;border:0;box-sizing:border-box;background-color:#fff;color:#000}#code-editor{height:300px;padding:1rem}.editor{height:302px}.code-viewer{height:auto;display:block}.code-wrap{position:relative;margin:0 auto;border:1px solid var(--device-color)}.device-controls{margin:20px auto;width:100%;display:inline-flex;align-items:baseline;justify-content:center}.device-btn{margin:0 20px;border:2px solid var(--device-border-color);background-color:var(--device-color);display:inline-block;position:relative;cursor:pointer;transition:background 0.3s ease}.device-btn:hover{background:var(--device-active-color)}.device-btn[data-device="mobile"]{width:41.4px;height:73.6px;border-top-width:5px;border-bottom-width:8px;border-radius:3px}.device-btn[data-device="mobile"]::before{width:17px;height:1px;top:-3px;margin-left:-8px}.device-btn[data-device="mobile"]::after{width:3px;height:3px;bottom:-5px;margin-left:-1px;border-radius:50%}.device-btn[data-device="tablet"]{width:82px;height:118px;border-top-width:5px;border-bottom-width:8px;border-radius:3px}.device-btn[data-device="tablet"]::before{width:1px;height:1px;top:-3px}.device-btn[data-device="tablet"]::after{width:3px;height:3px;bottom:-5px;margin-left:-1px;border-radius:50%}.device-btn[data-device="laptop"]{width:256px;height:160px;border-width:8px;border-radius:3px 3px 0 0}.device-btn[data-device="laptop"]::after{content:" ";background:#e8e8e8;width:120%;bottom:-10px;left:-10%;height:10px;border-radius:0 0 5px 5px}.browser-frame{position:relative;margin:20px auto;border-radius:5px}.browser-frame.mobile{width:377px;max-width:100%;max-height:669px;overflow:auto}.browser-frame.tablet{width:770px;max-width:100%;max-height:1026px;overflow:auto}.browser-frame.laptop{width:100%;max-height:90%;overflow:auto}@media screen and (min-width:481px) and (max-width:774px){.device-btn[data-device="laptop"]{display:none}}@media screen and (max-width:480px){.device-controls{display:none}}@media (prefers-color-scheme:dark){#code-editor,.code-viewer{background-color:#000;color:#fff}}
Javascript
document.addEventListener("DOMContentLoaded",function (){var iframe=document.querySelector('.code-viewer');var textarea=document.getElementById('code-editor');if (!textarea || !iframe) return;function createHTMLDocument(content,cssContent=''){return `<!DOCTYPE html>\n<html>\n<head>\n<meta charset="UTF-8">\n<title>VIEWER</title>\n<style>\n@charset "utf-8";\nhtml,body{\nmargin:0;\npadding:0;\nbox-sizing:border-box\n}\n${cssContent}\n</style>\n</head>\n<body>\n${content}\n</body>\n</html>`}function adjustIframeHeight(){if (iframe && iframe.contentWindow){requestAnimationFrame(function (){try{var iframeDoc=iframe.contentDocument || iframe.contentWindow.document;var newHeight=Math.max(iframeDoc.body.offsetHeight,iframeDoc.documentElement.offsetHeight);console.log('New iframe height:',newHeight);if (iframe.style.height !==newHeight+'px'){iframe.style.height=newHeight+'px';console.log('Iframe height adjusted to:',newHeight)}}catch (e){console.error('높이 조정 실패:',e)}})}}function setupEventListeners(){var deviceControls=document.querySelector('.device-controls');if (deviceControls){deviceControls.addEventListener('click',function (e){var button=e.target.closest('.device-btn');if (!button) return;e.preventDefault();var deviceType=button.getAttribute('data-device');var browser=document.querySelector('.browser-frame');if (browser){browser.className='browser-frame code-wrap '+deviceType;console.log('Device type changed to:',deviceType);updateIframe();adjustIframeHeight()}})}}function getInitialCode(){var markupCode=document.querySelector('code.language-markup');var cssCode=document.querySelector('code.language-css');var jsCode=document.querySelector('code.language-js, code.language-javascript');return createHTMLDocument((markupCode ? markupCode.textContent:'')+(jsCode ? '\n<script>\n'+jsCode.textContent+'\n</script>':''),cssCode ? cssCode.textContent:'')}function updateIframe(){var iframeDoc=iframe.contentDocument || iframe.contentWindow.document;try{iframeDoc.open();iframeDoc.write(createHTMLDocument(textarea.value));iframeDoc.close();console.log('Iframe content updated');adjustIframeHeight()}catch (e){console.error('Iframe 업데이트 실패:',e)}}function initIframe(){textarea.value=getInitialCode();updateIframe();adjustIframeHeight()}var inputTimeout;textarea.addEventListener('input',function (){clearTimeout(inputTimeout);inputTimeout=setTimeout(function(){updateIframe();adjustIframeHeight()},500)});setupEventListeners();initIframe()});
위 Javascript에서 (jsCode ? ‘\n<script>\n’+jsCode.textContent+’\n</script>’:”) 코드 부분의 </script>는 HTML 페이지 내부에 <script> … </script> 사이에 Javascript 코드도 같이 넣으려면 <\/script>라고 입력해야 정상적으로 작동한다.
그렇지 않고 그냥 js 파일을 새로 생성하여 페이지에 추가하는 것이라면 그냥 </script> 라고 넣으면 정상 작동한다.