여러분이 사용하고 계신 브라우저는 HTML5를 지원하지 않기 때문에 몇몇 요소가 제대로 보이도록 JScript를 사용하고 있습니다. 하지만 여러분의 브라우저 설정에서 스크립트 기능이 꺼져있으므로, 현재 페이지를 제대로 확인하시려면 스크립트 기능을 켜주셔야 합니다. 온라인 에디터 & 뷰어를 만들어보자.

온라인 에디터 & 뷰어를 만들어보자.

1개월전 작성

블로그로 코드를 공유하고 바로 그 페이지에서 바로 에디터 확인하여 코드 수정하고 실시간으로 반영되어 볼 수 있게 뷰어도 만들어보자.

처음엔 iframe 만들어서 썼는데 매일 iframe 만들어서 공유하는 것은 정말 쉽지 않은 것 같더라….

온라인 에디터 & 뷰어

jsdelivr 나 codepen 같은 사이트에서 제공하는 것처럼 최초에 저장되었던 코드가 나오고 그 코드를 수정할 수 있는 에디터 + 실시간 결과가 나오는 뷰어를 만들고 싶다.

어떻게 해야 코드를 수정 가능하게 공유하고 실시간으로 코드를 적용해서 보여줄 수 있을지 방법을 알아보자.

먼저 어떤 기능으로 만들 것인지 정리해보자.

  1. 구문 강조(Syntax Highlighter)를 위해 이용한 <code></code> 안에 들어가 있는 코드를 활용하자.
  2. 코드를 에디터에 모아 놓고 수정할 수 있어야 한다.
  3. 에디터에 들어간 코드는 자동으로 iframe에서 열 수 있게 하자.
  4. 내가 에디터에 들어간 코드를 수정하면 iframe에 바로 반영되게 하자.
  5. 에디터와 뷰어는 항상 같이 사용하자.
  6. 뷰어는 모바일, 태블릿, 컴퓨터 버튼을 만들어 놓고 각각 기기를 누르면 그 각각의 기기에선 어떻게 보이는지 대략적으로 확인할 수 있다.
  7. 모바일에서는 다른 기기를 볼 필요가 없으므로 기기 선택하는 메뉴가 안 나오게 하고 태블릿에서도 컴퓨터의 화면을 볼 순 없으니 컴퓨터 아이콘이 보이지 않게 한다.
  8. iframe, textarea에 border:0; 이어도 작동이 잘 되어야한다.
  9. iframe의 높이는 내부 높이를 확인하여 자동 조절 되어야한다. 그런데 border가 생기면 스크롤바가 무조건 생기니까 border 없이 무조건 작동되어야한다.
  10. 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">&lt;h2&gt;안녕하세요.&lt;/h2&gt;
&lt;p&gt;환영합니다.&lt;/p&gt;
&lt;p id="date"&gt;&lt;/p&gt;</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> 라고 넣으면 정상 작동한다.

보기

Mingg`s Diary
밍구
밍구
공부 목적 블로그