GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

코드 에디터 만들기 9종 — 온라인 코드 편집기 핵심 기법

코드 에디터 만들기는 웹 페이지 안에서 코드를 치면 색이 입고 줄 번호가 붙고 괄호가 저절로 닫히는 입력칸을 짜는 일입니다. 아홉 개를 라이브러리 없이 textarea 하나로 만들었고, 아래 격자에서 지금 돌아갑니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

순서는 인기순이 아니라 입력칸 하나가 에디터가 되어 가는 순서입니다. 먼저 글자에 색이 들어가고(01), 그 옆에 줄 번호가 붙고(02), 치는 동안 괄호가 따라오고(03), 잡은 줄을 한꺼번에 미는 법이 생기고(04), 문서가 길어지면 전체 지도가 필요해지고(05), 찾아 바꿀 일이 생기고(06), 치다 만 낱말을 완성해 주고(07), 틀린 곳에 밑줄이 깔리고(08), 마지막에 눈이 편한 색으로 갈아입습니다(09). 아홉 개 모두 <textarea> 가 진짜 입력이고, 색은 그 아래 깔린 <pre> 가 그립니다. 같은 embed 기둥에서 입력이 아니라 명령 출력이 주인공인 터미널 UI, 코드 대신 언어팩 JSON 을 검사하는 홈페이지 다국어 처리 검수가 이 편의 이웃입니다.

01실시간 구문 강조

글자를 투명하게 만든 textarea 아래에 같은 서체·같은 행간의 <pre> 를 겹쳐 두고, 입력이 들어올 때마다 정규식 네 줄로 색을 다시 칠하는 문서 사이트용 코드 입력칸입니다.

실시간 구문 강조
새 창에서 열기

01 · 실시간 구문 강조 — 측정 누적면적 25.074% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

-webkit-text-fill-colorwhite-space: prescrollTop

규칙 네 줄의 순서가 곧 우선순위

// 토크나이저 규칙 네 줄. 앞의 규칙이 먼저 이긴다 — 주석 안의 색 코드를 색으로 잡지 않으려는 순서다
var RULES = [
  { cls: 'c', re: /\/\*[\s\S]*?\*\/|\/\/[^\n]*/g },
  { cls: 's', re: /"[^"\n]*"|'[^'\n]*'/g },
  { cls: 'n', re: /#[0-9a-fA-F]{3,8}\b|\b\d+(?:\.\d+)?(?:px|ms|s|%|em|rem|fr)?\b/g },
  { cls: 'k', re: /--[a-zA-Z][\w-]*|\.[a-zA-Z][\w-]*|@[a-z-]+|\b[a-z-]+(?=\s*:)/g }
];

먼저 잡힌 자리는 뒤 규칙이 못 건드리므로 /* #ff4d1f 는 주석이다 */ 를 넣으면 색 코드가 아니라 주석 한 덩이로 칠해집니다.

두 겹이 어긋나지 않게 하는 값

%layer {
  box-sizing: border-box; margin: 0; padding: $sp-2;
  border: 0; border-radius: $r-sm;
  font-family: $mono; font-weight: 500; font-size: $fs; line-height: $lh;
  letter-spacing: 0; tab-size: 2;
  white-space: pre; overflow: auto; overscroll-behavior: contain;
}

서체·크기·행간·패딩·테두리·자간·탭 폭·줄바꿈 규칙을 한 곳에 모아서 두 요소가 함께 쓰는데, Playwright 로 계산값 여덟 항목을 비교하니 다른 값이 0개였습니다.

02줄 번호 거터와 현재 줄

왼쪽 거터의 번호가 입력 줄 수를 따라 늘고 줄며 커서가 있는 줄만 배경이 켜지는, 오류 줄을 짚어야 하는 편집기용 부품입니다.

줄 번호 거터와 현재 줄
새 창에서 열기

02 · 줄 번호 거터와 현재 줄 — 측정 누적면적 24.353% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

counter-resetselectionStarttranslateY

번호는 CSS 카운터가 매긴다

// 줄 수가 바뀔 때마다 거터를 다시 그린다 — 번호 매김은 scss 의 counter-reset 카운터가 한다
function render() {
  var lines = ta.value.split('\n');
  var nums = '';
  lines.forEach(function () { nums += '<span class="gt__num"></span>'; });
  gtrack.innerHTML = nums + '<span class="gt__gcur"></span>';
  gcurEl = gtrack.querySelector('.gt__gcur');
  hl.innerHTML = lines.map(function (l) {
    return '<span class="gt__ln">' + (esc(l) || ' ') + '</span>';
  }).join('');
  countEl.textContent = LBL_COUNT.replace('{n}', lines.length);
  update();
}

빈 <span> 만 줄 수만큼 찍고 숫자는 counter-reset: gtline 과 counter-increment 가 붙이는데, 여섯 줄짜리에 두 줄을 더 치니 번호가 6개에서 8개로 늘었습니다.

현재 줄은 selectionStart 로 센다

// selectionStart 까지의 글자에서 몇째 줄인지 계산해 두 밴드를 같은 값으로 옮긴다
function update() {
  var idx = ta.value.slice(0, ta.selectionStart).split('\n').length - 1;
  var y = idx * lineH();
  curEl.style.transform = 'translateY(' + y + 'px)';
  if (gcurEl) gcurEl.style.transform = 'translateY(' + y + 'px)';
  sync();
}

거터 밴드와 본문 밴드가 같은 translateY 값을 받으므로, 본문을 40px 내리면 두 트랙 모두 translateY(-40px) 로 따라옵니다.

03괄호·따옴표 자동 닫기

여는 괄호를 치면 닫는 짝이 함께 들어가고 커서는 그 사이에 남는, 짧은 스니펫을 빨리 치는 입력칸용 부품입니다.

괄호·따옴표 자동 닫기
새 창에서 열기

03 · 괄호·따옴표 자동 닫기 — 측정 누적면적 7.31% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

setSelectionRangepreventDefaultkey

짝 삽입·통과·짝 삭제 세 갈래

var over = s === t && v.charAt(s) === k && (PAIRS[k] === k || ')}]'.indexOf(k) >= 0);
if (PAIRS[k] && !over) {
  // 여는 짝 — 닫는 짝을 같이 넣고 setSelectionRange 로 커서는 그 사이에 남긴다
  e.preventDefault();
  var inner = v.slice(s, t);
  ta.setRangeText(k + inner + PAIRS[k], s, t, 'end');
  ta.setSelectionRange(s + 1, s + 1 + inner.length);
  setState('짝 삽입');
} else if (over) {
  // 이미 닫혀 있는 자리 — 삽입하지 않고 커서만 넘어간다
  e.preventDefault();
  ta.setSelectionRange(s + 1, s + 1);
  setState('통과');
}

fade( 까지 치면 값이 fade() 가 되고 커서는 5번 자리에 서며, 거기서 ) 를 한 번 더 쳐도 값은 그대로이고 커서만 6번으로 넘어갑니다. a{} 가운데에서 Backspace 를 누르면 두 글자가 함께 지워져 a 만 남습니다.

04Tab 들여쓰기와 되돌리기

Tab 은 두 칸을 넣고 Shift+Tab 은 두 칸을 빼며, 여러 줄을 잡고 누르면 잡힌 줄 전체가 같이 움직이는 설정 파일 편집용 부품입니다.

Tab 들여쓰기와 되돌리기
새 창에서 열기

04 · Tab 들여쓰기와 되돌리기 — 측정 누적면적 16.572% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

selectionEndsplittab-size

잡은 줄 전체를 한 번에 민다

var lines = v.slice(ls, ne).split(NL);
var firstShift = 0, lastShift = 0;
var moved = lines.map(function (ln, i) {
  if (dir > 0) {
    if (i === 0) firstShift = INDENT.length;
    if (i === lines.length - 1) lastShift = INDENT.length;
    return INDENT + ln;
  }
  var lead = ln.match(/^ {1,2}/);
  var cut = lead ? lead[0].length : 0;
  if (i === 0) firstShift = -cut;
  if (i === lines.length - 1) lastShift = -cut;
  return ln.slice(cut);
});
ta.value = v.slice(0, ls) + moved.join(NL) + v.slice(ne);
// 선택 범위 재계산: 시작은 첫 줄의 밀린 만큼, 끝은 마지막 줄의 밀린 만큼 따라간다
ta.setSelectionRange(Math.max(ls, s + firstShift), Math.max(ls + 1, e + lastShift));

3~5줄을 잡고(24~62번 글자) Tab 을 누르니 줄 앞 공백이 0,0,2,2,2 가 되고 선택은 26~64로 밀렸으며, Shift+Tab 으로 되돌리면 0,0,0,0,0 과 24~62 로 정확히 돌아왔고 지울 것이 없는 상태에서 한 번 더 눌러도 값과 선택이 그대로 남았습니다.

05캔버스 미니맵

오른쪽 좁은 캔버스가 글자 대신 줄 길이를 픽셀 띠로 그려 문서 전체 모양을 보여 주는, 긴 파일에서 위치를 잡는 편집기용 부품입니다.

캔버스 미니맵
새 창에서 열기

05 · 캔버스 미니맵 — 측정 누적면적 15.017% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

fillRectgetContextscrollHeight

글자를 그리지 않고 길이만 그린다

var pitch = h / LINES.length;
var barH = Math.max(1.5, pitch - 1.5);
LINES.forEach(function (ln, i) {
  var bw = Math.max(2, (ln.trim().length / maxLen) * (w - 6));
  ctx.fillStyle = ln.indexOf('//') === 0 ? 'rgba(190,183,173,.40)' : 'rgba(255,247,230,.78)';
  ctx.fillRect(3, i * pitch, bw, barH);
});

줄마다 글자 수를 가장 긴 줄로 나눠 띠 길이로 쓰는데, 그려진 픽셀은 2,452개였고 상자를 끝까지 끌면 본문 scrollTop 이 0에서 376으로 전체 범위를 움직였습니다. 반대로 본문을 120 스크롤하면 상자가 35.7px 내려와 비율이 맞습니다.

06찾기·바꾸기 바

Ctrl+F 로 바가 내려오고 맞는 자리마다 노란 표시가 깔리는, 긴 설정을 한 번에 고치는 화면용 부품입니다.

찾기·바꾸기 바
새 창에서 열기

06 · 찾기·바꾸기 바 — 측정 누적면적 15.163% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

matchAllmarkscrollIntoView

자리를 한 번에 뽑아 번호를 매긴다

// matchAll 로 모든 자리를 한 번에 뽑고, 몇 번째인지 셀 수 있게 시작·끝 인덱스를 남긴다
function hits() {
  if (!q.value) return [];
  var re = new RegExp(escRe(q.value), 'g');
  return Array.from(ta.value.matchAll(re)).map(function (m) {
    return { s: m.index, e: m.index + m[0].length };
  });
}

4px 를 치면 표시가 6개 깔리고 카운터가 1/6, Enter 를 누르면 2/6 으로 옮겨 가며, 바꾸기를 한 번 누르면 지금 자리 하나만 8px 가 되어 4px 는 5개로 줄고 카운터가 2/5 로 다시 세어집니다.

07커서 자리 자동완성

치던 낱말로 목록이 좁혀지고 팝업이 커서 바로 아래에 뜨는, 정해진 키워드를 치는 쿼리 입력칸용 부품입니다.

커서 자리 자동완성
새 창에서 열기

07 · 커서 자리 자동완성 — 측정 누적면적 8.792% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

role="listbox"aria-activedescendantstartsWith

커서 x 좌표는 거울 요소로 잰다

function caretX(pos) {
  mirror.textContent = ta.value.slice(0, pos);
  var pad = parseFloat(getComputedStyle(ta).paddingLeft) || 0;
  return pad + mirror.offsetWidth - ta.scrollLeft;
}
// 팝업 왼쪽을 커서 x 에 맞춘다 — 오른쪽 끝을 넘으면 안으로 접는다
function place(pos) {
  var x = caretX(pos);
  var maxX = ta.clientWidth - pop.offsetWidth - 4;
  pop.style.left = Math.max(0, Math.min(x, Math.max(0, maxX))) + 'px';
}

같은 서체로 커서 앞 글자만 담은 숨은 요소를 두고 그 폭을 읽는데, document.quer 까지 쳤을 때 팝업은 화면 155px 자리에 떠서 입력칸 왼쪽 끝(54px)이 아니라 치던 글자 옆에 섰습니다. 후보는 12개에서 2개로 좁혀지고 aria-activedescendant 가 ac-opt0 에서 아래 화살표 한 번에 ac-opt1 로 바뀌며, Enter 를 누르면 치던 조각이 통째로 document.querySelectorAll 로 갈립니다.

08오류 물결 밑줄과 툴팁

괄호가 안 닫혔거나 색 코드 자릿수가 틀리면 그 자리에 빨간 물결 밑줄이 깔리는, 붙여 넣은 코드를 바로 점검하는 칸용 부품입니다.

오류 물결 밑줄과 툴팁
새 창에서 열기

08 · 오류 물결 밑줄과 툴팁 — 측정 누적면적 32.71% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

text-decoration-styletext-decoration-skip-inkaria-describedby

규칙 두 개짜리 간이 린트

function lint(text) {
  var errs = [], m;
  var hexRe = /#[0-9a-zA-Z]+/g;
  while ((m = hexRe.exec(text)) !== null) {
    var body = m[0].slice(1);
    var ok = /^[0-9a-fA-F]+$/.test(body) && HEX_DIGITS.indexOf(body.length) >= 0;
    if (!ok) errs.push({ start: m.index, end: m.index + m[0].length, msg: MSG_HEX });
  }
  var stack = [];
  for (var i = 0; i < text.length; i++) {
    var ch = text.charAt(i);
    if (ch === '{' || ch === '(' || ch === '[') stack.push(i);
    else if (ch === '}' || ch === ')' || ch === ']') {
      if (stack.length) stack.pop();
      else errs.push({ start: i, end: i + 1, msg: MSG_BRACKET });
    }
  }
  stack.forEach(function (p) { errs.push({ start: p, end: p + 1, msg: MSG_BRACKET }); });
  errs.sort(function (a, b) { return a.start - b.start; });
  return errs;
}

정상 코드를 넣으면 오류 수가 0, #ff412 처럼 다섯 자리 색 코드를 넣으면 1이 되고 툴팁이 자릿수를 짚어 주며, 물결 밑줄은 text-decoration: underline wavy 두 낱말이고 오류 수는 상태 줄 글자로도 남아 aria-describedby 로 입력칸에 연결됩니다.

09에디터 테마 스위처

라이트·다크·고대비 셋을 CSS 변수 한 묶음으로 갈아 끼워 토큰 색까지 한 번에 바꾸는, 야간 작업이 잦은 관리 화면용 부품입니다.

에디터 테마 스위처
새 창에서 열기

09 · 에디터 테마 스위처 — 측정 누적면적 61.065% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

data-theme--tok-keycolor-scheme

변수를 등록해야 컷이 깨끗하다

// 색 변수를 등록한다 — 등록된 <color> 변수만 keyframes 에서 steps(1, end) 로 값이 딱 끊어진다.
// 초깃값은 라이트 벌(정적 기본 상태)과 같은 값이다
@property --ed-bg  { syntax: '<color>'; inherits: true; initial-value: #{$stage-paper}; }
@property --ed-fg  { syntax: '<color>'; inherits: true; initial-value: #{$subject-ink}; }
@property --tok-key { syntax: '<color>'; inherits: true; initial-value: #{$subject-blue-deep}; }
@property --tok-str { syntax: '<color>'; inherits: true; initial-value: #7440d4; }
@property --tok-num { syntax: '<color>'; inherits: true; initial-value: #{$error}; }
@property --tok-com { syntax: '<color>'; inherits: true; initial-value: #6b6577; }

JS 가 하는 일은 data-theme 한 글자를 바꾸는 것뿐이고 색 여섯 개와 color-scheme 은 선택자가 갈아 끼우는데, 고대비 판은 글자마다 실제로 재서 본문 21.0:1, 키워드 14.54:1, 문자열 11.88:1, 숫자 11.37:1, 주석 12.92:1 이 나왔습니다.

온라인 코드 편집기는 라이브러리 없이 어디까지 되나?

여기까지가 라이브러리 없이 되는 범위입니다. 코드 편집기 웹 서비스를 본뜰 때도 여러 파일, 되돌리기 기록 관리, 언어 서버 연동처럼 문서 모델이 필요한 부분부터는 전용 편집기가 맞습니다. 반대로 입력칸 하나에 색·번호·괄호·찾기·자동완성만 얹는 정도라면 텍스트 에디터 만들기의 재료는 <textarea> 와 <pre> 두 요소면 충분합니다. 받은 파일을 그대로 열면 브라우저에서 바로 돕니다.

어디서 깨지는가 — 미리보기는 도는데 손에서는 안 도는 코드

격자에 보이는 움직임은 CSS 키프레임이고 사람이 치는 순간은 JS 경로입니다. 둘이 별개 코드라 한쪽만 살아 있어도 측정은 통과합니다.

죽은 동기 코드가 조용히 통과했다

미리보기 루프는 CSS 가 돌리고 실제 조작은 JS 가 받는데, 게이트가 보는 것은 앞쪽뿐이라 뒤쪽이 죽어 있어도 측정값은 멀쩡하게 나옵니다. 02 는 본문이 스크롤하면 거터를 같은 값으로 미는 함수를 갖고 있었는데, textarea 를 내용만큼 늘어나게 둔 탓에 스크롤하는 것은 textarea 가 아니라 바깥 상자였습니다. 12줄을 넣고 재니 scrollHeight 와 clientHeight 가 둘 다 256으로 같았고, scrollTop 을 40으로 놓아도 거터는 translateY(0) 그대로였습니다. textarea 를 격자 칸 높이에 묶고 나서야 본문 40에 거터 -40이 따라왔습니다. 05 의 미니맵처럼 좌표를 직접 계산해 그리는 화면은 css 그래프 만들기 SVG로 9종과 같은 함정을 공유합니다 — 그리는 쪽은 맞는데 재는 대상이 틀린 경우입니다. 압축 파일 아홉 개의 비밀번호는 8j2t7h4k 이고, 같은 이유로 아홉 개 전부 .is-demo 를 뗀 정적 경로와 실제 키 입력을 따로 쟀습니다.

컷은 구간 끝에서만 값을 바꾼다

01 의 첫 판은 0% { opacity: 0 } 100% { opacity: 1 } 에 steps(1, end) 를 걸었습니다. 한 구간짜리 컷은 구간 끝에서 값을 바꾸므로 2초 내내 투명했고, 대표 이미지가 빈 화면으로 나왔습니다. 3% 에 점프 지점을 따로 두자 누적면적이 0.26%에서 25.07%로, 움직인 프레임이 4/23에서 15/23으로 올랐습니다. 06 도 같은 함정이라 매치 표시를 처음부터 켜 두고 움직임은 현재 자리 이동으로 만들었습니다.

상태 글자를 JS 로만 쓰면 미리보기에서 멈춘다

04·05·09 는 상태 문장을 JS 가 textContent 로 쓰는데, 미리보기 루프에서는 그 코드가 안 돌아 첫 문장이 그대로 남았습니다. 대표 이미지가 「1번 줄 커서」인데 화면에는 3~5줄 선택 밴드가 깔린 장면이 되었습니다. 문장을 두 장 겹쳐 두고 CSS 가 고르게 바꾸고, 05 처럼 이동이 연속인 것은 미리보기 문장에서 숫자를 뺐습니다.

변형 세 가지와 접근성

변수 세 개만 바꾸면 아홉 개의 성격이 함께 바뀝니다.

이름 바꾼 값 느낌
duration $dur-loop 2s → 1s 타이핑과 팝업이 두 배로 빨라진다
easing $ease-spring → $ease-pop 팝업과 바가 한 번 더 튄다
color $subject-blue → $stage-yellow 토큰 강조색이 차가운 쪽에서 따뜻한 쪽으로

입력은 전부 진짜 <textarea> 라 스크린리더·IME·되돌리기 경로가 그대로 살아 있습니다. 07 의 팝업은 role="listbox" 와 aria-activedescendant 로 알리되 포커스는 입력칸에 남겨 계속 칠 수 있고, 08 은 오류 수를 글자로 적어 aria-describedby 로 연결합니다. prefers-reduced-motion: reduce 에서는 아홉 개 모두 미리보기 루프와 커서 깜빡임이 멈추고 그 부품이 가장 잘 읽히는 정지 화면 한 장이 남습니다. 대비는 아홉 개 화면의 글자 60개를 렌더 픽셀로 재서 AA 미달 0개를 확인했습니다.

더 읽을거리: 코드 블록 UI 9종 은 읽기 전용 코드 표시를, 터미널·CLI UI 9종 은 명령줄 화면을 다룹니다. 명세는 MDN textarea 문서와 setSelectionRange 문서를 보면 됩니다.

FAQ

코드 에디터 웹 화면에 Monaco 나 CodeMirror 를 쓰는 게 낫지 않나요

여러 파일과 언어 서버가 필요하면 그쪽이 맞습니다. 입력칸 하나에 색과 번호만 필요한데 수백 KB짜리 라이브러리를 싣는 것은 과합니다. 아홉 개 중 필요한 것만 골라 붙이면 추가 의존성이 0입니다.

한글이나 일본어를 칠 때 조합 중 글자가 깨지지 않나요

<textarea> 를 진짜 입력으로 두었기 때문에 조합은 브라우저가 처리합니다. contenteditable 로 바꾸면 조합 중 DOM 을 건드리다가 글자가 끊기는데, 이 방식은 오버레이만 다시 그리므로 입력 경로를 건드리지 않습니다. 고칠 수 없고 읽기만 하는 코드 창이 필요하다면 코드 블록 UI 9종이 더 가벼운 답입니다.

물결 밑줄은 오래된 브라우저에서도 보이나요

text-decoration-style 은 caniuse 기준 전역 96.65%(완전 0.03% + 부분 96.62%)이고 Chrome 57+, Edge 79+, Safari 7.1+, Firefox 6+ 에서 씁니다(2026-09-13 확인). Internet Explorer 와 Opera Mini 에는 없으니 오류 수를 글자로도 적어 두는 편이 안전합니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.