코딩테스트 JS 고급: 최소 힙으로 다익스트라 최단 경로 구하기

2026.09.10·수정 2026.09.13·약 24분·작성: 해비·블로그 소개

먼저 직접 풀어보세요

최단 경로를 강조한 그래프와 최소 힙 구조를 표현한 다익스트라 학습 이미지

방향이 있는 길의 비용이 모두 0 이상일 때, 출발점에서 각 지점까지 이동하는 최소 비용을 구하는 독립 실습입니다. 문제를 먼저 읽고 풀이와 정답은 아래 접힌 영역에서 확인하세요.

선수 학습과 학습 목표

Node.js 표준 입력과 배열 순회는 JavaScript 코딩테스트 자료구조 로드맵, 자료구조에 남는 상태를 설명하는 연습은 올바른 괄호 검증에서 복습하세요. 배열의 인덱스, 함수, 클래스 기본 문법을 알고 시작합니다. 그래프의 인접 리스트와 최소 힙은 이 글에서 새로 설명합니다. 목표는 우선순위 큐를 직접 만들고 최단거리 갱신, 오래된 후보 제거, 음수 간선 제한을 코드와 근거로 설명하는 것입니다.

문제: 출발점에서 모든 지점까지의 최소 비용

정점은 1부터 N까지 번호가 붙어 있습니다. 출발점 S에서 다른 모든 정점으로 가는 최소 비용을 구하세요. u에서 v로 비용 w에 이동하는 간선은 역방향 이동을 보장하지 않습니다. 같은 두 정점 사이의 여러 간선, 자기 자신으로 향하는 간선, 비용이 0인 간선도 허용합니다. 도달할 수 없는 정점은 INF를 출력합니다. 이 문제는 본문 실습을 위해 구성한 자체 문제입니다.

항목 계약
첫 줄 N M: 1 ≤ N ≤ 20,000, 0 ≤ M ≤ 200,000
둘째 줄 출발점 S: 1 ≤ S ≤ N
이후 M줄 u v w: 1 ≤ u, v ≤ N, 0 ≤ w ≤ 1,000,000인 정수
출력 1번부터 N번 정점까지 순서대로 최소 비용 또는 INF를 한 줄씩 출력합니다. 출발점은 0입니다.

입력 예제

6 8
1
1 2 10
1 3 2
3 2 3
2 4 1
3 4 9
4 5 0
1 5 20
3 5 15

출력 예제

0
5
2
6
6
INF

2번까지는 1 → 3 → 2의 비용이 5입니다. 4번과 5번은 이 경로에 이어서 각각 비용 1과 0을 더하므로 모두 6입니다. 6번으로 향하는 경로는 없습니다. 먼저 비용이 큰 직행 간선과 작은 비용의 우회 경로가 함께 있을 때 어떤 후보부터 처리해야 하는지 적어 보세요.

풀이와 정답

풀이와 정답 보기 (클릭)

1. 그래프와 우선순위 큐를 준비합니다

graph[u]에 [v, w]를 저장하면 u에서 나가는 간선만 확인할 수 있습니다. 일반 큐는 들어온 순서대로 꺼내지만, 이 문제의 우선순위 큐는 [현재까지의 비용, 정점] 중 비용이 가장 작은 항목을 꺼냅니다. 모든 간선의 비용이 같지 않으므로 간선 개수가 적은 경로가 최소 비용이라는 보장은 없습니다.

최소 힙은 배열로 저장하는 완전 이진 트리입니다. 인덱스 i의 부모는 floor((i-1)/2), 자식은 2i+1과 2i+2입니다. 부모 비용이 두 자식 비용보다 작거나 같게 유지하면 맨 앞에 최솟값이 옵니다. 삽입은 끝에 놓고 부모 방향으로 올리고, 삭제는 마지막 값을 루트 자리에서 작은 자식 방향으로 내립니다. 같은 비용의 정점은 어느 것을 먼저 꺼내도 됩니다. 이 구현은 자식 중 더 작은 쪽을 고르며 힙 전체를 정렬하지 않습니다. 힙의 기본 원리는 Princeton Algorithms: Priority Queues를 참고했습니다.

2. 거리 갱신과 오래된 후보 처리를 분리합니다

dist[S]만 0, 나머지는 Infinity로 시작합니다. 비용 cost인 정점 u에서 간선 (u, v, w)를 확인할 때 cost+w가 dist[v]보다 작으면 거리와 후보를 갱신합니다. 이를 완화(relaxation)라고 부릅니다. 더 짧아진 경우에만 넣는 엄격한 비교가 필요합니다. 같을 때도 다시 넣으면 0 비용 순환에서 같은 후보가 끝없이 생길 수 있습니다.

이 코드는 힙 내부 항목을 찾아 값을 줄이는 decrease-key 대신 새 후보를 삽입합니다. 예제에서 [10, 2]를 먼저 넣어도 나중에 [5, 2]가 생깁니다. [5, 2]를 확장한 뒤 [10, 2]가 나오면 cost !== dist[vertex]로 버립니다. 오래된 후보에서 간선을 다시 읽지 않아 반복 작업을 막습니다.

꺼낸 후보 처리
[0, 1] 2번=10, 3번=2, 5번=20 후보 추가
[2, 3] 2번=5, 4번=11, 5번=17로 개선
[5, 2] 4번=6으로 개선
[6, 4] 5번=6으로 개선
[6, 5] 더 확장할 간선 없음
[10, 2], [11, 4], [17, 5], [20, 5] 현재 거리와 다르므로 모두 건너뜀

3. 왜 최소 후보를 꺼내도 안전한가요?

유효한 최소 후보 u보다 더 짧은 미발견 경로가 있다고 가정해 봅니다. 그 경로에서 이미 처리한 영역을 처음 벗어나는 정점 x는 앞선 간선 완화로 후보가 되었어야 합니다. 남은 가중치가 모두 0 이상이므로 x까지의 비용은 그 더 짧은 경로 전체보다 클 수 없습니다. 그러면 u보다 작은 후보 x가 먼저 나와야 하므로 모순입니다. 따라서 유효한 최소 후보의 거리는 확정할 수 있습니다. 정의와 알고리즘의 비음수 조건은 Princeton Algorithms: Shortest Paths에서 확인할 수 있습니다.

음수 간선이 있으면 위 근거가 성립하지 않습니다. 예를 들어 1→2가 2, 1→3이 5, 3→2가 -10이면 먼저 나온 2의 비용 2가 최종 최솟값이 아닙니다. 이 글의 solve는 음수 간선을 거부합니다. dijkstra 함수 자체는 검증을 통과한 비음수 그래프를 받는 내부 함수입니다. 음수를 처리하려면 Bellman-Ford 등 조건에 맞는 알고리즘과 음수 순환 정책을 별도로 설계해야 합니다.

4. Node.js 표준 입력 전체 코드

아래 코드를 algorithm-advanced.cjs로 저장합니다. 외부 패키지는 필요 없습니다. readFileSync(0, 'utf8')로 표준 입력을 읽고, 공백·탭·LF·CRLF를 같은 토큰 경계로 처리합니다. 읽기 API는 Node.js 공식 파일 시스템 문서를 따릅니다. 잘못된 입력은 표준 출력 없이 stderr와 종료 코드 1로 알립니다. .cjs 확장자는 CommonJS 실행을 명시합니다.

'use strict';
const fs = require('node:fs');

class MinHeap {
  constructor() { this.items = []; }
  get size() { return this.items.length; }
  push(entry) {
    const a = this.items;
    a.push(entry);
    let i = a.length - 1;
    while (i > 0) {
      const parent = Math.floor((i - 1) / 2);
      if (a[parent][0] <= entry[0]) break;
      a[i] = a[parent];
      i = parent;
    }
    a[i] = entry;
  }
  pop() {
    const a = this.items;
    if (a.length === 0) return undefined;
    const root = a[0];
    const last = a.pop();
    if (a.length === 0) return root;
    let i = 0;
    while (i * 2 + 1 < a.length) {
      let child = i * 2 + 1;
      if (child + 1 < a.length && a[child + 1][0] < a[child][0]) child++;
      if (last[0] <= a[child][0]) break;
      a[i] = a[child];
      i = child;
    }
    a[i] = last;
    return root;
  }
}

function dijkstra(graph, start) {
  const dist = Array(graph.length).fill(Infinity);
  const heap = new MinHeap();
  dist[start] = 0;
  heap.push([0, start]);
  while (heap.size > 0) {
    const [cost, vertex] = heap.pop();
    if (cost !== dist[vertex]) continue; // 예전 거리의 항목은 확장하지 않습니다.
    for (const [next, weight] of graph[vertex]) {
      const candidate = cost + weight;
      if (candidate < dist[next]) {
        dist[next] = candidate;
        heap.push([candidate, next]);
      }
    }
  }
  return dist;
}

function solve(input) {
  const text = input.trim();
  if (!text) throw new Error('입력이 비었습니다.');
  const raw = text.split(/\s+/);
  if (!raw.every(token => /^-?\d+$/.test(token))) {
    throw new Error('정수 토큰만 허용합니다.');
  }
  const tokens = raw.map(Number);
  if (!tokens.every(Number.isSafeInteger)) throw new Error('안전한 정수가 아닙니다.');
  const [n, m, start] = tokens;
  if (!(n >= 1 && n <= 20000 && m >= 0 && m <= 200000 &&
        start >= 1 && start <= n && tokens.length === 3 + 3 * m)) {
    throw new Error('정점·간선 개수 또는 시작점이 올바르지 않습니다.');
  }
  const graph = Array.from({ length: n + 1 }, () => []);
  for (let i = 3; i < tokens.length; i += 3) {
    const [from, to, weight] = tokens.slice(i, i + 3);
    if (!(from >= 1 && from <= n && to >= 1 && to <= n &&
          weight >= 0 && weight <= 1000000)) {
      throw new Error('정점 범위 또는 가중치가 올바르지 않습니다. 음수 간선은 허용하지 않습니다.');
    }
    graph[from].push([to, weight]); // 방향 간선이므로 역방향은 추가하지 않습니다.
  }
  return dijkstra(graph, start).slice(1)
    .map(value => value === Infinity ? 'INF' : String(value)).join('\n');
}

if (require.main === module) {
  try {
    process.stdout.write(solve(fs.readFileSync(0, 'utf8')) + '\n');
  } catch (error) {
    process.stderr.write(error.message + '\n');
    process.exitCode = 1;
  }
}
module.exports = { MinHeap, dijkstra, solve };

입력 예제를 input.txt로 저장한 뒤 Windows 명령 프롬프트 또는 Bash에서 아래와 같이 실행합니다. PowerShell에서는 Get-Content -Raw -Encoding UTF8 .\input.txt | node .\algorithm-advanced.cjs를 사용할 수 있습니다. 답 이외의 디버깅 문장을 표준 출력에 추가하지 마세요.

node algorithm-advanced.cjs < input.txt

5. 시간·공간 복잡도와 숫자 범위

유효한 후보에서 각 정점의 나가는 간선을 한 번씩 확인합니다. 성공한 완화는 최대 M번이고, 시작점까지 포함해 힙 삽입·삭제는 O(M+1)번입니다. 오래된 후보도 남기므로 힙 최대 크기는 O(M+1)이며, 힙 연산은 O(log(M+2))입니다. 정점 초기화·출력까지 포함한 시간은 O(N + (M+1) log(M+2)), 인접 리스트·거리·힙·입력 토큰을 포함한 공간은 이 제한에서 O(N+M)입니다. M=0에서도 초기화와 출력 O(N)이 필요합니다.

다중 간선을 허용하는 이 구현을 무조건 O(M log N), 힙 공간 O(N)이라고 적지 않습니다. 별도의 인덱스 힙으로 정점당 후보 하나만 유지하는 구현과 구분해야 합니다. 정수의 자릿수가 고정되어 있지 않다면 파싱에 입력 문자열 길이 L에 따른 시간·공간 비용도 추가해야 합니다.

비음수 그래프의 최단 경로는 순환을 제거한 단순 경로로 고를 수 있습니다. 따라서 최단거리 최댓값은 (N-1)×1,000,000=19,999,000,000이고 Number의 안전한 정수 범위 안입니다. 후보 덧셈도 N×1,000,000 이하입니다. 32비트 정수 범위는 넘을 수 있으므로 비용에 비트 연산이나 | 0을 쓰면 안 됩니다. 제한을 늘리면 안전 정수 범위를 다시 계산하세요.

6. 경계 테스트와 완료 기준

검증 기대 결과·조건
본문 예제 0, 5, 2, 6, 6, INF
N=1, M=0, S=1 0
N=3, M=0, S=2 INF, 0, INF
0 비용 순환 1→2→1과 2→3 모든 거리 0, 반복 종료
1→1 비용0, 1→2 비용9·2, 2→3 비용4 0, 2, 6
간선 1→2, 출발점2 INF, 0: 역방향 이동 금지
동일 최소 비용의 두 경로 같은 거리를 유지하고 불필요하게 재삽입하지 않음
음수·잘못된 정점·부족한 토큰 입력 오류
정점20,000개의 비용1,000,000 체인 끝 정점 19,999,000,000
간선200,000개의 다중 간선 최소 간선 비용1 선택

검증 파일 algorithm-advanced.test.cjs는 고정 사례 7개를 LF·CRLF로 검사하고 실제 Node 프로세스 14회에서 stdout·stderr·종료 코드를 확인합니다. 잘못된 입력 7개, 힙의 빈 상태·중복 우선순위, 오래된 후보의 인접 리스트 재방문 방지, 최대 N·M도 검사합니다. 시드 20260910의 작은 무작위 그래프 500개는 독립적인 Bellman-Ford 기준 풀이와 비교합니다. HTML에 실린 코드와 실행 파일이 같은지도 검사합니다. 함께 제공된 테스트 파일을 같은 폴더에 두고 node algorithm-advanced.test.cjs로 재현할 수 있습니다.

완료 기준은 예제 출력이 맞는 것에 더해, 음수 간선을 허용하지 않는 이유, 오래된 후보를 버리는 이유, 도달 불가 출력, 다중 간선을 포함한 복잡도를 자신의 말로 설명하는 것입니다. 추가 연습으로 예제의 간선 하나를 제거하고 거리 변화를 손으로 예상한 뒤 실행 결과와 비교해 보세요.

검증 기록: 2026-09-10, Node.js v24.18.0에서 위 검증을 모두 통과했습니다. 아래를 algorithm-advanced.test.cjs로 저장하면 재현할 수 있습니다. HTML 동기화 검사는 작성자 검토용이므로 아래 독자용 테스트에서는 제외했습니다.

'use strict';
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const path = require('node:path');
const fs = require('node:fs');
const { MinHeap, dijkstra, solve } = require('./algorithm-advanced.cjs');
const cases = [
 ['example', '6 8\n1\n1 2 10\n1 3 2\n3 2 3\n2 4 1\n3 4 9\n4 5 0\n1 5 20\n3 5 15\n', '0\n5\n2\n6\n6\nINF'],
 ['single', '1 0\n1', '0'],
 ['no edges / non-one start', '3 0\n2', 'INF\n0\nINF'],
 ['zero cycle', '3 3\n1\n1 2 0\n2 1 0\n2 3 0', '0\n0\n0'],
 ['parallel edges / self-loop', '3 4\n1\n1 1 0\n1 2 9\n1 2 2\n2 3 4', '0\n2\n6'],
 ['direction', '2 1\n2\n1 2 7', 'INF\n0'],
 ['equal candidates', '4 4\n1\n1 2 1\n1 3 1\n2 4 1\n3 4 1', '0\n1\n1\n2'],
];
for (const [name, input, expected] of cases) {
  for (const variant of [input, input.replace(/\n/g, '\r\n')]) {
    assert.equal(solve(variant), expected, name);
    const run = spawnSync(process.execPath, [path.join(__dirname, 'algorithm-advanced.cjs')], { input: variant, encoding: 'utf8' });
    assert.equal(run.status, 0, name);
    assert.equal(run.stdout, expected + '\n', name);
    assert.equal(run.stderr, '', name);
  }
}
const invalid = ['', '2 1 1 1 2 -1', '2 1 1 1 2', '2 0 3', '2 1 1 1 3 2', '2 1 1 1 2 1.5', '2 1 1 1 2 1000001'];
for (const input of invalid) assert.throws(() => solve(input));
const rejected = spawnSync(process.execPath, [path.join(__dirname, 'algorithm-advanced.cjs')], { input: invalid[1], encoding: 'utf8' });
assert.equal(rejected.status, 1);
assert.equal(rejected.stdout, '');
assert.match(rejected.stderr, /음수 간선/);
const heap = new MinHeap();
assert.equal(heap.pop(), undefined);
for (const cost of [5, 1, 9, 1, 0, 6, 2]) heap.push([cost, cost]);
assert.deepEqual(Array.from({length: 7}, () => heap.pop()[0]), [0, 1, 1, 2, 5, 6, 9]);
// 독립 기준 풀이: 모든 간선을 N-1번 완화하는 Bellman-Ford.
function reference(n, edges, start) {
 const d = Array(n + 1).fill(Infinity); d[start] = 0;
 for (let k = 1; k < n; k++) {
  for (const [u, v, w] of edges) d[v] = Math.min(d[v], d[u] + w);
 }
 return d;
}
let seed = 20260910;
function random(n) { seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0; return seed % n; }
for (let k = 0; k < 500; k++) {
 const n = 1 + random(9), m = random(50), start = 1 + random(n);
 const edges = Array.from({length: m}, () => [1 + random(n), 1 + random(n), random(21)]);
 const graph = Array.from({length: n + 1}, () => []);
 for (const [u,v,w] of edges) graph[u].push([v,w]);
 assert.deepEqual(dijkstra(graph, start), reference(n, edges, start));
}
// stale entry가 인접 리스트를 재확장하지 않는지 실제 방문 횟수 확인.
let visits = 0;
const g = [[], [[2, 10], [3, 1]], [], [[2, 1]]];
g[2] = { *[Symbol.iterator]() { visits++; } };
dijkstra(g, 1);
assert.equal(visits, 1);
// 최대 정점 수와 가중치: 32비트 범위를 초과하는 정상 최단거리.
const n = 20000;
const chain = Array.from({length: n - 1}, (_, i) => (i + 1) + ' ' + (i + 2) + ' 1000000');
assert.equal(solve(n + ' ' + (n - 1) + '\n1\n' + chain.join('\n')).split('\n').at(-1), '19999000000');
// 최대 간선 수와 다중 간선.
const many = Array.from({length: 200000}, (_, i) => '1 2 ' + (200000 - i)).join('\n');
assert.equal(solve('2 200000\n1\n' + many), '0\n1');
// HTML 동기화 검사는 작성자의 로컬 초안 검증에서 별도로 실행했습니다.
console.log('PASS: 7 fixed cases x LF/CRLF; 14 CLI runs; 7 invalid; heap; 500 Bellman-Ford comparisons; stale-entry; max V/E');


풀이 전에 확인할 순서

  1. 입력값과 출력값을 한 문장으로 다시 적습니다.
  2. 반복할 대상과 비교·저장할 값을 정합니다.
  3. 필요한 자료구조와 시간복잡도를 예상합니다.
  4. 코드를 보기 전에 손으로 작은 예제를 계산합니다.
힌트: 코딩테스트 JS 고급: 최소 힙으로 다익스트라 최단 경로 구하기

정답 코드를 바로 따라 쓰기보다, 본문에서 값이 갱신되는 조건과 반복 범위를 먼저 찾으세요. 반복 한 번마다 반드시 유지되어야 하는 값이 무엇인지 적으면 풀이의 중심 변수를 고르기 쉽습니다.

테스트 확인

  • 가능한 가장 작은 입력
  • 같은 값이나 문자가 반복되는 입력
  • 정답이 처음 또는 마지막 위치에서 결정되는 입력
  • 입력 제한에 가까운 경우의 실행 시간

확인 결과: 본문의 예제뿐 아니라 위 경계 사례에서도 예상값과 실제 출력이 같아야 풀이가 완료됩니다.

이 글이 도움이 되었나요?

조회 중

코딩테스트 JavaScript 학습 순서

필수 49개 · 전체 49개

읽음 기록 관리

전체 과정 목차 (49개)
  1. 필수 길잡이 · 코딩테스트 JS 자료구조 로드맵: 배열, 해시, 스택, 투 포인터 순서
  2. 필수 학습 · 세 수 중 최솟값 JavaScript 조건문 풀이 정리
  3. 필수 학습 · 삼각형 판별하기 JavaScript 풀이
  4. 필수 학습 · 연필 개수 JavaScript 풀이
  5. 필수 학습 · 1부터 N까지 합 출력하기 JavaScript 풀이
  6. 필수 학습 · 최솟값 구하기 JavaScript 풀이|배열 순회와 비교 갱신 원리
  7. 필수 학습 · 홀수 JavaScript 풀이: 조건 판별과 결과 처리 정리
  8. 필수 학습 · 10부제 JavaScript 풀이: 끝자리 비교로 위반 차량 수 세기
  9. 필수 학습 · A를 #으로 JavaScript 풀이: 문자열 순회와 치환
  10. 필수 학습 · 문자 찾기 JavaScript 풀이: 문자열 순회로 개수 세기
  11. 필수 학습 · 대문자 찾기 JavaScript 풀이
  12. 필수 학습 · 대문자로 통일 JavaScript 풀이
  13. 필수 학습 · 대소문자 변환 JavaScript 풀이
  14. 필수 학습 · 일곱 난쟁이 JavaScript 풀이: 두 명을 제외하는 완전탐색
  15. 필수 학습 · 코딩테스트 JS Map 풀이: 학급 회장 득표수 세기
  16. 필수 학습 · 코딩테스트 JS 스택 풀이: 올바른 괄호 검증하기
  17. 필수 학습 · 코딩테스트 JS 스택 풀이: 괄호문자 제거하기
  18. 필수 학습 · 코딩테스트 JS 스택 풀이: 크레인 인형뽑기 처리법
  19. 필수 학습 · 코딩테스트 JS 스택 풀이: 후위식 연산 계산하기
  20. 필수 학습 · 코딩테스트 JS 스택 풀이: 쇠막대기 레이저 절단 개수 세기
  21. 필수 학습 · 코딩테스트 JS 투 포인터 풀이: 두 정렬 배열 합치기
  22. 필수 학습 · 코딩테스트 JS 투 포인터 풀이: 공통 원소 추출하기
  23. 필수 학습 · 코딩테스트 JS 슬라이딩 윈도우 풀이: 최대 매출 구간 합 계산하기
  24. 필수 학습 · JavaScript 투 포인터: 합이 M인 연속 부분수열 개수
  25. 필수 학습 · 코딩테스트 JS 해시 풀이: 모든 아나그램 찾기
  26. 필수 학습 · 가장 긴 문자열 JavaScript 풀이
  27. 필수 학습 · 가운데 문자 출력 JavaScript 풀이
  28. 필수 학습 · 중복문자제거 JavaScript 풀이
  29. 필수 학습 · 코딩테스트 JS 고급: 최소 힙으로 다익스트라 최단 경로 구하기 현재 글
  30. 필수 학습 · 코딩테스트 JS Union-Find: 연결 성분 수와 크기 구하기
  31. 필수 학습 · 코딩테스트 JS Trie: 접두사에 맞는 단어 수 세기
  32. 필수 학습 · 코딩테스트 JS Fenwick Tree: 값 갱신과 구간 합 처리
  33. 필수 학습 · 코딩테스트 JS 세그먼트 트리: 단일 대입과 구간 합
  34. 필수 학습 · 코딩테스트 JS LRU 캐시: 지도 타일 재사용 기록
  35. 필수 학습 · 코딩테스트 JS AVL 트리: 기준 이상 최솟값 찾기
  36. 필수 학습 · 코딩테스트 JS 큐: 상담 창구 대기열 명령 처리
  37. 필수 학습 · 코딩테스트 JS 연결 리스트: 재생 대기 목록 관리
  38. 필수 학습 · JavaScript 원형 덱 연습: 최근 기록 창과 되돌리기
  39. 필수 학습 · JavaScript 해시 테이블 연습: 정규화 문자열 빈도와 등장 순서
  40. 필수 학습 · JavaScript 트리 순회 연습: 깊이별 노드 묶기
  41. 필수 학습 · JavaScript BST 연습: 닫힌 구간의 중복 키 보고서
  42. 필수 학습 · JavaScript 최소 힙 연습: 동률 순서를 지키는 작업 스케줄러
  43. 필수 학습 · JavaScript 그래프 연습: 연결 구역 크기를 작은 순서로 출력하기
  44. 필수 학습 · 중복단어제거 JavaScript 풀이
  45. 필수 학습 · TypeScript 이진 탐색 연습: 숫자 카드 존재 여부 확인
  46. 필수 학습 · 큰 수 출력하기 JavaScript 풀이
  47. 필수 학습 · 보이는 학생 JavaScript 풀이
  48. 필수 학습 · 가위바위보 JavaScript 풀이
  49. 필수 학습 · 점수계산 JavaScript 풀이

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기