15분 만에 이 JS 계산기를 만들어 보세요! 🖩 | Bro Code


브라우저 기반 코드 에디터 구축 가이드

1. 프로젝트 개요

1.1 프로젝트 소개

이 가이드는 완전히 브라우저에서 실행되는 경량 코드 에디터를 구축하는 방법을 다룹니다. HTML, CSS, JavaScript를 위한 탭형 에디터를 제공하며, 샌드박스 iframe에서 즉석 라이브 미리보기를 지원합니다.

1.2 주요 기능

  • 탭형 에디터: HTML, CSS, JavaScript 각각을 위한 독립적인 탭
  • 라이브 미리보기: 샌드박스 iframe을 통한 즉시 결과 확인
  • 키보드 단축키: 빠르고 부드러운 에디터 조작
  • 유효성 검사: JavaScript 코드 검증을 위한 검사 기능
  • 저장/로드 기능: JSON 다운로드 및 로컬 스토리지 복원
  • 접근성: ARIA 상태, 포커스 관리, 완전한 키보드 단축키
  • 신뢰성: CDN 고정 및 graceful fallback 적용

1.3 사용 장면

  • 수업용 교육 도구
  • 과제 및 연습용 플랫폼
  • 빠른 프로토타이핑
  • 온라인 코딩 환경

2. 프로젝트 시연 및 기능 소개

2.1 인터페이스 구조

프로젝트는 다음과 같은 구조로 구성됩니다:

헤더 섹션

  • 로고 및 제목 (“Code Editor Live in Browser”)
  • Save 및 Load 버튼

메인 섹션 (3개 하위 섹션)

  1. 사이드바: 과제/할당 패널

    • 과제 설명 텍스트 영역
    • JavaScript 전용 유효성 검사 (선택사항)
  2. 중앙 에디터: HTML/CSS/JavaScript 에디터

    • Run 버튼: 모든 코드를 한 번에 실행
    • “Open Preview in Window” 버튼: 새 창에서 미리보기
  3. 출력 패널: 결과 및 로그 표시

    • “Run with Tests” 버튼
    • “Clear Log” 버튼

2.2 실제 사용 예제

색상 전환기 예제

  • HTML로 기본 구조 생성
  • CSS로 스타일링 적용
  • JavaScript로 색상 변경 기능 구현
  • 실시간으로 빨강, 초록, 파랑 색상 전환 가능

할일 목록 예제

  • HTML로 입력 폼과 목록 구조 생성
  • CSS로 레이아웃 스타일링
  • JavaScript로 동적 할일 추가/완료 기능
  • 완료된 항목에 취소선 표시

랜덤 명언 생성기 예제

  • 버튼 클릭 시 랜덤 명언 표시
  • 동적 콘텐츠 업데이트 기능

RGB 색상 믹서 예제

  • 빨강, 초록, 파랑 채널별 슬라이더
  • 실시간 색상 조합 미리보기
  • 색상 값 표시

2.3 저장 및 로드 기능

  • 저장: JSON 형식으로 프로젝트 다운로드 (academy-web.json)
  • 로드: JSON 파일을 선택하여 이전 작업 복원
  • 로컬 스토리지: 브라우저 종료 후에도 작업 내용 유지

3. HTML 구조 구축

3.1 기본 HTML5 구조

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Code Editor - Programming Ocean Academy</title>
</head>
<body>
    <!-- 컨텐츠 구조 -->
</body>
</html>

3.2 헤더 요소 구축

컨테이너 구조

  • div.container.row: 행 방향 레이아웃 컨테이너
  • div.brand: 브랜드 정보 (로고 + 제목)
  • div.row: 버튼 컨테이너

브랜드 영역

<div class="brand">
    <div class="logo"></div>
    <div class="title">Code Editor Live in Browser</div>
</div>

버튼 영역

<div class="row">
    <button class="btn secondary" id="save-btn" title="Save work locally">
        <span>Save</span>
    </button>
    <button class="btn secondary" id="load-btn" title="Load work file">
        <span>Load</span>
    </button>
    <input type="file" id="open-file" accept="application/json" hidden>
</div>

3.3 메인 섹션 구축

사이드바 (과제 패널)

<aside class="card panel stack">
    <h2>Task/Assignment</h2>
    <textarea id="assignment" class="text" 
              placeholder="Write the task description here..."></textarea>
    
    <div class="stack">
        <label for="test-area">Validation Tests (JavaScript only - optional)</label>
        <textarea id="test-area" class="text" 
                  placeholder="Write simple tests to append after the student's code to check the solution..."></textarea>
    </div>
    
    <div class="muted">
        <span>Tip: <span class="kbd">Ctrl + S</span> to save, 
               <span class="kbd">Ctrl + Enter</span> to run</span>
    </div>
</aside>

웹 에디터 섹션

<section class="stack" id="web-only">
    <div id="web-editors" class="stack panel card">
        <div class="row">
            <h2>HTML/CSS/JavaScript</h2>
            <div class="row">
                <button class="btn" id="run-web">Run</button>
                <button class="btn secondary" id="open-preview">Preview</button>
            </div>
        </div>
        
        <!-- 탭 네비게이션 -->
        <div class="tabs" id="web-tabs" role="tablist" aria-label="Web editors">
            <button class="tab active" role="tab" 
                    aria-selected="true" tabindex="0" 
                    data-pane="html">HTML</button>
            <button class="tab" role="tab" 
                    aria-selected="false" tabindex="-1" 
                    data-pane="css">CSS</button>
            <button class="tab" role="tab" 
                    aria-selected="false" tabindex="-1" 
                    data-pane="javascript">JavaScript</button>
        </div>
        
        <!-- 에디터 래퍼들 -->
        <div class="editor-wrap" data-pane="html">
            <div id="editor-html" class="editor"></div>
        </div>
        <div class="editor-wrap" data-pane="css" hidden>
            <div id="editor-css" class="editor"></div>
        </div>
        <div class="editor-wrap" data-pane="javascript" hidden>
            <div id="editor-js" class="editor"></div>
        </div>
        
        <!-- 미리보기 -->
        <div class="stack">
            <h3>Preview</h3>
            <iframe id="preview" class="preview" 
                    sandbox="allow-scripts allow-same-origin allow-modals allow-forms">
            </iframe>
        </div>
    </div>
</section>

출력 패널

<aside class="card panel stack">
    <h2>Output</h2>
    <div id="output" class="out" aria-live="polite"></div>
    
    <div class="row">
        <button class="btn warn" id="run-tests">Run with Tests</button>
        <button class="btn secondary" id="clear-log">Clear Log</button>
    </div>
    
    <h3>Notes</h3>
    <ul class="muted">
        <li>Everything runs inside the browser in a sandbox</li>
        <li>You can save the work as a JSON file and restore it</li>
    </ul>
</aside>

3.4 시맨틱 HTML 요소의 중요성

main 요소

  • HTML5 시맨틱 요소
  • 웹 페이지의 주요 콘텐츠를 나타냄
  • 한 문서당 하나만 사용해야 함
  • 보조 기술에게 주요 콘텐츠 영역을 알려줌

접근성 속성

  • role="tablist": 탭 목록임을 명시
  • aria-label: 보조 기술용 라벨
  • aria-selected: 선택된 탭 상태
  • aria-live="polite": 동적 콘텐츠 변경 알림

3.5 CSS 및 JavaScript 파일 연결

<!-- CSS 연결 -->
<link rel="stylesheet" href="styles.css">

<!-- JavaScript 연결 (body 끝부분) -->
<script src="script.js"></script>

4. CSS 스타일링 및 레이아웃

4.1 CSS 변수 정의

:root {
    --background: #0F121B;
    --body-background: #0B0F1D;
    --panel: #141B2B;
    --muted: #8B93A7;
    --txt: #E9ECF1;
    --brand: #7D3FC7;
    --brand-2: #6B0A5A;
    --btn-ok: #22C55E;
    --btn-warn: #EAB308;
    --btn-error: #EF4444;
    --radius: 18px;
    --gap: 14px;
}

4.2 전역 리셋

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

html, body {
    height: 100%;
}

4.3 바디 스타일링

body {
    font-family: sans-serif;
    background: linear-gradient(180deg, 
                var(--body-background) 35%, 
                var(--txt));
    color: var(--txt);
}

4.4 헤더 스타일링

header {
    padding: 20px;
    background: linear-gradient(90deg, 
                rgba(96, 165, 250, 0.15), 
                rgba(125, 211, 252, 0.1));
    border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}

4.5 컨테이너 및 레이아웃

컨테이너

.container {
    max-width: 1200px;
    margin-inline: auto;
    padding: 14px;
    justify-content: space-between;
}

브랜드 영역

.brand {
    display: flex;
    align-items: center;
    gap: 10px;
    font-weight: 700;
}

.brand .logo {
    inline-size: 36px;
    block-size: 36px;
    border-radius: 10px;
    background: url('https://programmingocean.com/codeeditors/new-logo.png') 
                center/cover no-repeat;
}

.title {
    font-size: clamp(18px, 2vw, 22px);
}

4.6 그리드 레이아웃 시스템

main {
    max-width: 1560px;
    margin-inline: auto;
    padding: 18px;
    display: grid;
    grid-template-columns: 280px 1fr 360px;
    gap: var(--gap);
}

/* 반응형 미디어 쿼리 */
@media (max-width: 1100px) {
    main {
        grid-template-columns: 1fr;
    }
}

4.7 카드 및 패널 스타일링

.card {
    background: linear-gradient(180deg, 
                rgba(255, 255, 255, 0.05), 
                rgba(255, 255, 255, 0.03));
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: var(--radius);
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
}

.panel {
    padding: 16px;
}

4.8 플렉스박스 레이아웃

스택 레이아웃 (세로 방향)

.stack {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

행 레이아웃 (가로 방향)

.row {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    justify-content: space-between;
}

4.9 버튼 스타일링

.btn {
    appearance: none;
    border: none;
    background: linear-gradient(180deg, var(--brand-2), var(--brand));
    color: #081F2F;
    font-weight: 700;
    padding: 10px 14px;
    border-radius: 12px;
    cursor: pointer;
    box-shadow: 0 10px 24px rgba(96, 165, 250, 0.25);
}

.btn:hover {
    transform: translateY(-2px);
    box-shadow: 0 15px 30px rgba(96, 165, 250, 0.4);
}

.btn.secondary {
    background: rgba(255, 255, 255, 0.1);
    color: var(--txt);
}

.btn.warn {
    background: var(--btn-warn);
    color: #1A1A1A;
}

4.10 탭 스타일링

.tabs {
    display: flex;
    gap: 4px;
    background: rgba(0, 0, 0, 0.3);
    padding: 4px;
    border-radius: 12px;
}

.tab {
    flex: 1;
    padding: 10px 16px;
    background: transparent;
    border: none;
    color: var(--muted);
    border-radius: 8px;
    cursor: pointer;
    transition: all 0.2s ease;
}

.tab.active {
    background: var(--brand);
    color: white;
}

4.11 에디터 및 미리보기 스타일링

.editor {
    height: 200px;
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.3);
}

.preview {
    width: 100%;
    height: 300px;
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 8px;
    background: white;
}

4.12 텍스트 영역 스타일링

.text {
    background: rgba(0, 0, 0, 0.3);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 8px;
    color: var(--txt);
    padding: 12px;
    font-family: inherit;
    resize: vertical;
    min-height: 100px;
}

.text::placeholder {
    color: var(--muted);
}

5. JavaScript 기능 구현

5.1 헬퍼 함수

DOM 선택 함수

const $ = (selector) => document.querySelector(selector);
const $$ = (selector) => document.querySelectorAll(selector);

const out = $('#output');
const preview = $('#preview');

HTML 이스케이프 함수

function escapeHtml(unsafe) {
    return unsafe
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
}

5.2 로깅 시스템

로그 출력 함수

function log(message, type = 'info') {
    const span = document.createElement('span');
    span.className = type === 'error' ? 'error' : 
                     type === 'warn' ? 'warn' : 'brand';
    span.textContent = message;
    out.appendChild(span);
}

function clearOut() {
    out.innerHTML = '';
}

5.3 Ace 에디터 설정

에디터 생성 함수

function makeEditor(mode) {
    const editor = ace.edit(document.createElement('div'));
    editor.setTheme('ace/theme/monokai');
    editor.session.setMode(mode);
    
    // 키 바인딩 설정
    editor.commands.addCommand({
        name: 'run',
        bindKey: { win: 'Ctrl-Enter', mac: 'Cmd-Enter' },
        exec: () => runWeb(false)
    });
    
    editor.commands.addCommand({
        name: 'save',
        bindKey: { win: 'Ctrl-S', mac: 'Cmd-S' },
        exec: saveProject
    });
    
    return editor;
}

에디터 인스턴스 생성

const tabOrder = ['html', 'css', 'javascript'];
const editors = {
    html: makeEditor('ace/mode/html'),
    css: makeEditor('ace/mode/css'),
    javascript: makeEditor('ace/mode/javascript')
};

5.4 탭 시스템

활성 탭 관리

function activePanes() {
    return [...$$('.tab')].filter(tab => 
        tab.getAttribute('aria-selected') === 'true'
    );
}

function activePane() {
    const actives = activePanes();
    return actives.length ? actives[0].dataset.pane : 'html';
}

function showPane(name) {
    // 모든 탭 비활성화
    $$('.tab').forEach(tab => {
        tab.classList.remove('active');
        tab.setAttribute('aria-selected', 'false');
        tab.tabIndex = -1;
    });
    
    // 선택된 탭 활성화
    const selectedTab = $(`.tab[data-pane="${name}"]`);
    selectedTab.classList.add('active');
    selectedTab.setAttribute('aria-selected', 'true');
    selectedTab.tabIndex = 0;
    
    // 에디터 패널 표시/숨김
    $$('.editor-wrap').forEach(wrap => {
        wrap.hidden = wrap.dataset.pane !== name;
    });
    
    // 에디터 리사이즈 및 포커스
    if (editors[name]) {
        requestAnimationFrame(() => {
            editors[name].resize(true);
            editors[name].focus();
        });
    }
}

5.5 키보드 네비게이션

$('#web-tabs').addEventListener('keydown', (e) => {
    if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
        e.preventDefault();
        const current = activePane();
        const currentIndex = tabOrder.indexOf(current);
        const delta = e.key === 'ArrowRight' ? 1 : -1;
        const newIndex = (currentIndex + delta + tabOrder.length) % tabOrder.length;
        showPane(tabOrder[newIndex]);
    }
});

$('#web-tabs').addEventListener('click', (e) => {
    const btn = e.target.closest('button');
    if (!btn) return;
    showPane(btn.dataset.pane);
});

5.6 코드 실행 시스템

HTML 문서 생성

function buildWebSrcDoc() {
    const html = editors.html.getValue();
    const css = editors.css.getValue();
    const js = editors.javascript.getValue();
    
    return `<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>${css}</style>
</head>
<body>
    ${html}
    <script>
        try {
            ${js}
        } catch (error) {
            console.error('JavaScript Error:', error);
        }
    </script>
</body>
</html>`;
}

코드 실행 함수

function runWeb(withTests = false) {
    try {
        const srcDoc = buildWebSrcDoc();
        preview.srcdoc = srcDoc;
        
        const message = withTests ? 'Run with tests' : 'Web preview updated';
        log(message);
        
        if (withTests) {
            const testCode = $('#test-area').value.trim();
            if (testCode) {
                // 테스트 코드 실행 로직
                log('Tests executed');
            }
        }
    } catch (error) {
        log(`Error: ${error.message}`, 'error');
    }
}

5.7 프로젝트 저장/로드 시스템

프로젝트 JSON 생성

function projectJSON() {
    return {
        version: 1,
        kind: 'web-only',
        assignment: $('#assignment').value,
        testArea: $('#test-area').value,
        html: editors.html.getValue(),
        css: editors.css.getValue(),
        javascript: editors.javascript.getValue()
    };
}

프로젝트 저장

function saveProject() {
    try {
        const data = JSON.stringify(projectJSON(), null, 2);
        
        // 로컬 스토리지에 저장
        localStorage.setItem(storageKey, data);
        
        // 파일 다운로드
        const blob = new Blob([data], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        
        const a = document.createElement('a');
        a.href = url;
        a.download = 'academy-web.json';
        a.click();
        
        URL.revokeObjectURL(url);
        log('Project saved');
    } catch (error) {
        log(`Save error: ${error.message}`, 'error');
    }
}

프로젝트 로드

function loadProject(data) {
    try {
        $('#assignment').value = data.assignment || '';
        $('#test-area').value = data.testArea || '';
        
        editors.html.setValue(data.html || '', -1);
        editors.css.setValue(data.css || '', -1);
        editors.javascript.setValue(data.javascript || '', -1);
        
        log('Project loaded');
    } catch (error) {
        log(`Load error: ${error.message}`, 'error');
    }
}

5.8 이벤트 리스너 설정

// 실행 버튼
$('#run-web').addEventListener('click', () => runWeb(false));
$('#run-tests').addEventListener('click', () => runWeb(true));

// 저장/로드 버튼
$('#save-btn').addEventListener('click', saveProject);
$('#load-btn').addEventListener('click', () => $('#open-file').click());

// 파일 선택
$('#open-file').addEventListener('change', (e) => {
    const file = e.target.files[0];
    if (!file) return;
    
    const reader = new FileReader();
    reader.onload = (e) => {
        try {
            const data = JSON.parse(e.target.result);
            loadProject(data);
        } catch (error) {
            log(`Invalid file: ${error.message}`, 'error');
        }
    };
    reader.readAsText(file);
});

// 기타 버튼
$('#clear-log').addEventListener('click', clearOut);
$('#open-preview').addEventListener('click', () => {
    const newWindow = window.open('about:blank');
    newWindow.document.write(buildWebSrcDoc());
});

5.9 로컬 스토리지 복원

const storageKey = 'academy-web-editor';

function setDefaultContent() {
    editors.html.setValue('', -1);
    editors.css.setValue('', -1);
    editors.javascript.setValue('', -1);
}

// 앱 초기화
try {
    const cache = localStorage.getItem(storageKey);
    if (cache) {
        loadProject(JSON.parse(cache));
    } else {
        setDefaultContent();
    }
} catch (e) {
    setDefaultContent();
}

log('Ready: Web-only editor HTML/CSS/JavaScript');

6. Ace Editor 라이브러리 통합

6.1 CDN 추가

HTML 파일의 script 태그 전에 Ace Editor CDN을 추가합니다:

<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/ace.js"></script>
<script src="script.js"></script>

6.2 Ace Editor 기능

  • 구문 강조: HTML, CSS, JavaScript 코드 하이라이팅
  • 자동 완성: 코드 자동 완성 기능
  • 코드 폴딩: 코드 블록 접기/펼치기
  • 테마 지원: 다양한 에디터 테마
  • 키바인딩: 다양한 에디터 키보드 단축키

7. 실용적인 팁 및 주의사항

7.1 개발 팁

  • CSS 변수 활용: 일관된 색상 및 크기 관리
  • 플렉스박스 사용: 반응형 레이아웃 구현
  • 그리드 레이아웃: 복잡한 2D 레이아웃 관리
  • 접근성 고려: ARIA 속성 및 키보드 네비게이션

7.2 성능 최적화

  • requestAnimationFrame: 에디터 리사이즈 시 사용
  • 이벤트 위임: 효율적인 이벤트 처리
  • 로컬 스토리지: 자동 진행상황 저장

7.3 보안 고려사항

  • iframe sandbox: 안전한 코드 실행 환경
  • HTML 이스케이프: XSS 공격 방지
  • JSON 파싱: 안전한 데이터 처리

7.4 브라우저 호환성

  • 모던 브라우저: ES6+ 기능 활용
  • CSS Grid: 최신 레이아웃 기술
  • Flexbox: 널리 지원되는 레이아웃

7.5 확장 가능성

  • 테마 시스템: CSS 변수로 쉬운 테마 변경
  • 언어 확장: 추가 프로그래밍 언어 지원
  • 플러그인 시스템: Ace Editor 플러그인 활용

8. 주요 키보드 단축키

  • Ctrl + Enter: 코드 실행
  • Ctrl + S: 프로젝트 저장
  • 화살표 키: 탭 간 이동
  • Tab: 포커스 이동

9. 프로젝트 완성 및 배포

이 가이드를 통해 완전히 기능하는 브라우저 기반 코드 에디터를 구축할 수 있습니다. 최종 결과물은:

  • 단일 HTML 페이지로 실행
  • 서버 없이 브라우저에서 완전 동작
  • 교육용 및 학습용으로 최적화
  • 반응형 디자인으로 모든 기기 지원

이 코드 에디터는 웹 개발 교육, 빠른 프로토타이핑, 온라인 코딩 환경으로 활용할 수 있는 강력하고 실용적인 도구입니다.

1개의 좋아요