jQuery를 사용한 Sudoku

인터넷과웹기초 (CB1501013-062)

May 27, 2026

0. HTML 구조

<!DOCTYPE html>
<html lang="ko">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>jQuery 4.0 실시간 검증 스도쿠 게임</title>

    <style>
        :root {
            --bg-gradient-start: #0f172a;
            --bg-gradient-end: #1e1b4b;
            --panel-bg: rgba(30, 41, 59, 0.7);
            --board-bg: #0f172a;
            --cell-bg-hint: #334155;
            --cell-bg-user: #1e293b;
            --cell-text-hint: #94a3b8;
            --cell-text-user: #60a5fa;
            --cell-focus-bg: #1e3a8a;
            --cell-focus-border: #3b82f6;
            --cell-conflict: #ef4444;
            --cell-conflict-bg: rgba(239, 68, 68, 0.2);
            --btn-primary: #3b82f6;
            --btn-primary-hover: #2563eb;
            --btn-secondary: #475569;
            --btn-secondary-hover: #334155;
            --text-main: #f8fafc;
            --text-muted: #cbd5e1;
        }

        body {
            margin: 0;
            padding: 0;
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            background: linear-gradient(135deg, var(--bg-gradient-start), var(--bg-gradient-end));
            font-family: system-ui, -apple-system, sans-serif;
            color: var(--text-main);
            overflow-x: hidden;
        }

        .game-wrapper {
            width: 90%;
            max-width: 500px;
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 1.5rem;
            padding: 1.5rem;
            background: var(--panel-bg);
            border-radius: 24px;
            box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
            backdrop-filter: blur(16px);
            border: 1px solid rgba(255, 255, 255, 0.1);
        }

        .header-section {
            text-align: center;
        }

        .header-section h1 {
            margin: 0;
            font-size: 1.8rem;
            letter-spacing: -0.05em;
            background: linear-gradient(to right, #60a5fa, #a78bfa);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }

        .header-section p {
            margin: 0.3rem 0 0;
            font-size: 0.9rem;
            color: var(--text-muted);
        }

        /* 9x9 스도쿠 보드 설정 */
        .board-container {
            width: 100%;
            aspect-ratio: 1 / 1;
            background: var(--board-bg);
            border: 3px solid #020617;
            border-radius: 12px;
            overflow: hidden;
            display: grid;
            grid-template-columns: repeat(9, 1fr);
            grid-template-rows: repeat(9, 1fr);
            box-shadow: inset 0 4px 8px rgba(0, 0, 0, 0.5);
        }

        /* 각 셀 스타일 정의 */
        .sudoku-cell {
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 1.4rem;
            font-weight: 700;
            font-family: 'Courier New', monospace;
            border: 0.5px solid rgba(255, 255, 255, 0.05);
            cursor: pointer;
            transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
            user-select: none;
        }

        /* 힌트 셀 스타일 */
        .hint-cell {
            background-color: var(--cell-bg-hint);
            color: var(--cell-text-hint);
            cursor: not-allowed;
        }

        /* 입력 가능 셀 스타일 */
        .user-input-cell {
            background-color: var(--cell-bg-user);
            color: var(--cell-text-user);
        }

        .user-input-cell:hover {
            background-color: rgba(255, 255, 255, 0.05);
        }

        /* 3x3 보더 라인 정의 */
        .border-right-bold {
            border-right: 2.5px solid #020617;
        }

        .border-bottom-bold {
            border-bottom: 2.5px solid #020617;
        }

        /* 포커스 및 충돌 하이라이팅 */
        .active-focus {
            background-color: var(--cell-focus-bg) !important;
            box-shadow: inset 0 0 0 2px var(--cell-focus-border);
            color: #ffffff !important;
        }

        .collision-alert {
            background-color: var(--cell-conflict-bg) !important;
            color: var(--cell-conflict) !important;
            animation: shake 0.2s ease-in-out 2 alternate;
        }

        @keyframes shake {
            0% {
                transform: translateX(0);
            }

            50% {
                transform: translateX(-2px);
            }

            100% {
                transform: translateX(2px);
            }
        }

        /* 컨트롤 영역 */
        .control-area {
            width: 100%;
            display: flex;
            flex-direction: column;
            gap: 1rem;
            align-items: center;
        }

        .pad-grid {
            display: grid;
            grid-template-columns: repeat(9, 1fr);
            gap: 0.4rem;
            width: 100%;
        }

        .pad-btn {
            aspect-ratio: 1 / 1;
            border-radius: 50%;
            border: none;
            background-color: var(--cell-bg-user);
            color: var(--cell-text-user);
            font-size: 1.2rem;
            font-weight: 700;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
            transition: all 0.15s ease;
            border: 1px solid rgba(255, 255, 255, 0.05);
        }

        .pad-btn:hover {
            transform: translateY(-2px);
            background-color: var(--btn-primary);
            color: #fff;
            box-shadow: 0 6px 12px rgba(59, 130, 246, 0.4);
        }

        .pad-btn:active {
            transform: translateY(0);
        }

        .actions-row {
            display: flex;
            gap: 0.5rem;
            width: 100%;
        }

        .action-btn {
            flex: 1;
            padding: 0.8rem;
            border-radius: 12px;
            border: none;
            font-weight: 600;
            font-size: 0.95rem;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 0.4rem;
            transition: all 0.15s ease;
        }

        .btn-reset {
            background-color: var(--btn-secondary);
            color: var(--text-main);
        }

        .btn-reset:hover {
            background-color: var(--btn-secondary-hover);
        }

        .btn-erase {
            background-color: rgba(239, 68, 68, 0.1);
            color: var(--cell-conflict);
            border: 1px solid rgba(239, 68, 68, 0.2);
        }

        .btn-erase:hover {
            background-color: rgba(239, 68, 68, 0.2);
        }

        /* 유리 모티프(Glassmorphism) 완료 모달 */
        .modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(15, 23, 42, 0.85);
            backdrop-filter: blur(12px);
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 100;
            opacity: 0;
            pointer-events: none;
            transition: opacity 0.3s ease;
        }

        .modal-overlay.show {
            opacity: 1;
            pointer-events: auto;
        }

        .modal-content {
            background: rgba(30, 41, 59, 0.9);
            border: 1px solid rgba(255, 255, 255, 0.15);
            border-radius: 28px;
            padding: 2.5rem;
            width: 85%;
            max-width: 400px;
            text-align: center;
            box-shadow: 0 25px 50px rgba(0, 0, 0, 0.6);
            transform: scale(0.9);
            transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
        }

        .modal-overlay.show .modal-content {
            transform: scale(1);
        }

        .modal-content h2 {
            margin: 0 0 0.5rem;
            font-size: 2rem;
            background: linear-gradient(to right, #34d399, #60a5fa);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }

        .modal-content p {
            margin: 0 0 1.5rem;
            color: var(--text-muted);
            line-height: 1.6;
        }

        .modal-btn {
            width: 100%;
            padding: 0.9rem;
            border-radius: 12px;
            border: none;
            background-color: var(--btn-primary);
            color: #ffffff;
            font-size: 1rem;
            font-weight: 600;
            cursor: pointer;
            transition: background-color 0.15s;
        }

        .modal-btn:hover {
            background-color: var(--btn-primary-hover);
        }
    </style>
</head>

<body>
    <div class="game-wrapper">
        <div class="header-section">
            <h1>jQuery 4.0 Sudoku</h1>
            <p>마우스 또는 키보드(1-9, Delete)로 스도쿠를 완성하세요</p>
        </div>

        <!-- 9x9 그리드 보드판 -->
        <div id="sudoku-board" class="board-container"></div>

        <!-- 입력 패널 및 컨트롤 컨트롤즈 -->
        <div class="control-area">
            <div id="number-pad" class="pad-grid">
                <button class="pad-btn">1</button>
                <button class="pad-btn">2</button>
                <button class="pad-btn">3</button>
                <button class="pad-btn">4</button>
                <button class="pad-btn">5</button>
                <button class="pad-btn">6</button>
                <button class="pad-btn">7</button>
                <button class="pad-btn">8</button>
                <button class="pad-btn">9</button>
            </div>

            <div class="actions-row">
                <button id="erase-btn" class="action-btn btn-erase">지우기</button>
                <button id="reset-btn" class="action-btn btn-reset">게임 재시작</button>
            </div>
        </div>
    </div>

    <!-- 완료 축하 모달 -->
    <div id="game-modal" class="modal-overlay">
        <div class="modal-content">
            <h2>스도쿠 완성!</h2>
            <p>축하합니다. 충돌 없이 올바르게 모든 수치 칸을 완성해내는 데 성공했습니다.</p>
            <button id="close-modal-btn" class="modal-btn">다시 도전하기</button>
        </div>
    </div>
    <!-- JS 관련 -->
</body>

</html>

1. jQuery 4.0 소개와 CDN 로드

jQuery 4.0은 현대적인 웹 표준을 수용하여 내부 구조를 대폭 리팩토링하고 ESM(ES Module) 지원을 핵심 기능으로 탑재했습니다. 기존에 사용되지 않는 레거시 메서드를 대거 정리하여 런타임 성능을 극대화했습니다. 이 튜토리얼에서는 로컬 파일(file://) 환경의 모듈 보안 제약 사항을 완벽히 피하고 안정적인 실행을 도모하기 위해 일반 CDN 스크립트 및 ESM 로딩 기법을 균형 있게 다룹니다.

    <!-- jQuery를 불러오세요 -->
    <script>
        $(function () {
            console.log('jQuery 4.0 slim UMD 로드 성공');
            // renderBoard();
            // initEventHandlers();
        });
    </script>

2. 핵심 데이터 구조

스도쿠 판의 힌트 수치 정보와 사용자가 임의로 입력하는 실시간 진행 상황 정보는 메모리상에서 상태 배열 형태로 온전하게 관리되어야 합니다.

// 스도쿠 퀴즈 원본 템플릿 (0은 비어 있는 격자를 의미)
const puzzleTemplate = [
  [5, 3, 0, 0, 7, 0, 0, 0, 0],
  [6, 0, 0, 1, 9, 5, 0, 0, 0],
  [0, 9, 8, 0, 0, 0, 0, 6, 0],
  [8, 0, 0, 0, 6, 0, 0, 0, 3],
  [4, 0, 0, 8, 0, 3, 0, 0, 1],
  [7, 0, 0, 0, 2, 0, 0, 0, 6],
  [0, 6, 0, 0, 0, 0, 2, 8, 0],
  [0, 0, 0, 4, 1, 9, 0, 0, 5],
  [0, 0, 0, 0, 8, 0, 0, 7, 9]
];
const puzzleTemplate = [
  [5, 3, 4, 6, 7, 8, 9, 1, 2],
  [6, 7, 2, 1, 9, 5, 3, 4, 8],
  [1, 9, 8, 3, 4, 2, 5, 6, 7],
  [8, 5, 9, 7, 6, 1, 4, 2, 3],
  [4, 2, 6, 8, 5, 3, 7, 9, 1],
  [7, 1, 3, 9, 2, 4, 8, 5, 6],
  [9, 6, 1, 5, 3, 7, 2, 8, 4],
  [2, 8, 7, 4, 1, 9, 6, 3, 5],
  [3, 4, 5, 2, 8, 6, 1, 7, 0]
];

3. 깊은 복사를 위한 상태 관리

// 게임 진행 과정에서 변경되는 실시간 입력판 배열
let currentBoard = JSON.parse(JSON.stringify(puzzleTemplate)); // 1 
let currentBoard = structuredClone(puzzleTemplate); // 2
  1. 별도의 외부 라이브러리(Lodash 등)를 설치하거나 복잡한 재귀 함수를 직접 구현하지 않고도, 표준 브라우저 환경에서 쉽고 간단하게 다차원 데이터를 깊은 복사할 수 있습니다.

  2. 복사 대상 데이터 내부에 함수, undefined, Symbol, 정규표현식 객체 등이 포함되어 있으면 JSON 변환 과정에서 데이터가 소실되거나 null로 변환되는 한계가 있습니다. 다만 스도쿠 게임처럼 순수 숫자와 배열로만 이루어진 정형 데이터 구조에서는 완벽하고 안전하게 작동합니다.

  3. 최근 모던 웹 표준 환경에서는 내장 API인 structuredClone 함수가 공식 지원되므로, let currentBoard = structuredClone(puzzleTemplate); 문법을 대안으로 사용하여 더 정밀하고 현대적인 깊은 복사를 수행할 수도 있습니다.

4. 상태 관리

// 사용자가 마우스로 선택한 활성 셀의 좌표 저장 객체
let selectedCell = { row: -1, col: -1 };
  1. 사용자가 보드의 빈 칸을 누르지 않은 상태에서 물리 키보드의 1~9 숫자를 타이핑하는 등 돌발적인 상호작용이 일어날 때, 시스템 오동작을 미연에 방지하는 핵심 조건문으로 기능합니다.

  2. 실제로 코드 내부에서 키 입력을 감수할 때 아래와 같이 차단 장치를 안전하게 적용할 수 있습니다.

if (selectedCell.row === -1 || selectedCell.col === -1) return;
  1. 이 가드 클로즈(Guard Clause) 덕분에 좌표가 지정되지 않은 초기 상태(-1, -1)에서는 어떠한 불필요한 숫자 주입 연산도 처리하지 않고 바로 함수를 조기 종료시켜 안전성을 확보하게 됩니다.
<script>
  $(function () {
    console.log('jQuery 4.0 slim UMD 로드 성공');
    renderBoard();
    initEventHandlers();
  });

  // 정답을 포함한 유효한 시작 퀴즈 원형 데이터 (0은 공백 셀)
  const puzzleTemplate = [
    [5, 3, 0, 0, 7, 0, 0, 0, 0],
    [6, 0, 0, 1, 9, 5, 0, 0, 0],
    [0, 9, 8, 0, 0, 0, 0, 6, 0],
    [8, 0, 0, 0, 6, 0, 0, 0, 3],
    [4, 0, 0, 8, 0, 3, 0, 0, 1],
    [7, 0, 0, 0, 2, 0, 0, 0, 6],
    [0, 6, 0, 0, 0, 0, 2, 8, 0],
    [0, 0, 0, 4, 1, 9, 0, 0, 5],
    [0, 0, 0, 0, 8, 0, 0, 7, 9]
  ];

  // 가변 동적 변경용 2차원 배열 데이터
  let currentBoard = JSON.parse(JSON.stringify(puzzleTemplate));

  // 사용자 클릭에 의한 선택 격자 상태 보존 객체
  let selectedCell = { row: -1, col: -1 };

  // ...
</script>

5. jQuery 4.0을 이용한 스도쿠 격자판 동적 렌더링

스도쿠 보드 영역에 격자를 출력하기 위해 2차원 배열 데이터를 기반으로 하여 jQuery 4.0 동적 요소 생성 메서드를 사용합니다. 이 코드는 데이터 상태 변화와 화면 표시 장치의 상태를 일관되게 정렬해 줍니다.

function renderBoard() {
  const $board = $('#sudoku-board');
  $board.empty(); // 이전 렌더링 격자 초기화

  for (let r = 0; r < 9; r++) {
    for (let c = 0; c < 9; c++) {
      const value = currentBoard[r][c];
      const isHint = puzzleTemplate[r][c] !== 0;

      // jQuery 4.0 스타일 체이닝 요소 작성
      const $cell = $('<div></div>')
        .addClass('sudoku-cell')
        .attr('data-row', r)
        .attr('data-col', c);

      if (value !== 0) {
        $cell.text(value);
      }

      // 수정 불가능한 힌트 수치와 유저 입력 수치 구분 스타일 추가
      if (isHint) {
        $cell.addClass('hint-cell');
      } else {
        $cell.addClass('user-input-cell');
        // 현재 선택된 셀이라면 포커스 강조 상태 지정
        if (selectedCell.row === r && selectedCell.col === c) {
          $cell.addClass('active-focus');
        }
      }

      // 3x3 굵은 테두리를 위한 위치 판별 논리식
      if (c === 2 || c === 5) $cell.addClass('border-right-bold');
      if (r === 2 || r === 5) $cell.addClass('border-bottom-bold');

      $board.append($cell);
    }
  }
}

6. jQuery 4.0 이벤트 위임을 이용한 셀 선택 상호작용

동적으로 생성된 스도쿠 칸 하나하나에 매번 개별 클릭 리스너를 결합하는 방식은 메모리 관리 관점에서 매우 비효율적입니다. 부모 노드인 보드판에 이벤트를 바인딩하고 jQuery 4.0 이벤트 위임 처리를 사용해 조작을 단순화합니다.

이 패턴은 동적 격자 재렌더링 시에도 매번 리스너를 재할당할 필요를 원천 제거해 주는 이점을 선사합니다.

function initEventHandlers() {
  const $board = $('#sudoku-board');

  // jQuery 4.0 이벤트 위임 API 패턴 구현
  $board.on('click', '.user-input-cell', function() {
    const $target = $(this);

    // 이전 활성 격자 하이라이트 스타일 제거 및 전이
    $('.sudoku-cell').removeClass('active-focus');
    $target.addClass('active-focus');

    // 메모리 내 활성 포커스 좌표 동기화
    selectedCell.row = parseInt($target.attr('data-row'), 10);
    selectedCell.col = parseInt($target.attr('data-col'), 10);
  });
}

7. 가상 숫자 패드와 키보드 동시 바인딩 처리

사용자가 손쉽게 숫자를 삽입할 수 있도록 화면상의 가상 숫자 패드 마우스 입력 이벤트와 물리 키보드의 숫자 키보드 수치 신호를 동시에 수렴할 수 있는 연계 바인딩을 설계합니다.

function initEventHandlers() {
  // ... 이전 격자 클릭 이벤트 바인딩 포함 ...

  // 가상 숫자 패드 마우스 클릭 이벤트 바인딩
  $('#number-pad').on('click', '.pad-btn', function() {
    const num = parseInt($(this).text(), 10);
    executeFill(num);
  });

  $('#erase-btn').on('click', function () {
      executeFill(0);
  });

  $('#reset-btn').on('click', function () {
      resetBoardData();
  }); 
  
  // 모달 리셋 액션 매핑
  $('#close-modal-btn').on('click', function() {
    $('#game-modal').removeClass('show');
    resetBoardData();
  });

  // 물리 키보드 단축키 감지 바인딩
  $(document).on('keydown', function(event) {
    if (selectedCell.row === -1 || selectedCell.col === -1) return;
    const key = event.key;
    if (key >= '1' && key <= '9') {
      executeFill(parseInt(key, 10));
    } else if (key === 'Backspace' || key === 'Delete' || key === '0') {
      executeFill(0);
    }
  });
}

8. 입력 검증

두 입력 인터페이스를 단일 데이터 처리 함수에 연결하여 개발상의 복잡성을 간소화합니다.

// 입력 값 주입 흐름 제어 함수
function executeFill(num) {
  const { row, col } = selectedCell;
  if (row === -1 || col === -1) return;
  if (puzzleTemplate[row][col] === 0) {
    currentBoard[row][col] = num;
    renderBoard();
    evaluateValidation();
    evaluateGameSuccess();
  }
}

9. 실시간 스도쿠 규칙 검증 논리 장치

사용자의 실시간 입력을 바탕으로 하여 중복된 숫자가 존재하는지 즉각 판별하고 규칙 위반 항목의 셀에 가시적으로 경고를 표현합니다. 이 규칙 검사 장치는 매 입력 시 동작하여 격자의 정합성을 즉시 사용자에게 시각적으로 알려 줍니다.

function evaluateValidation() {
  // 전체 셀의 기존 에러 표시 제거
  $('.sudoku-cell').removeClass('collision-alert');

  for (let r = 0; r < 9; r++) {
    for (let c = 0; c < 9; c++) {
      const value = currentBoard[r][c];
      if (value === 0) continue;

      if (detectCollision(r, c, value)) {
        $(`.sudoku-cell[data-row="${r}"][data-col="${c}"]`).addClass('collision-alert');
      }
    }
  }
}

function detectCollision(row, col, value) {
  // 행 내부 중복 수치 스캔
  for (let c = 0; c < 9; c++) {
    if (c !== col && currentBoard[row][c] === value) return true;
  }
  // 열 내부 중복 수치 스캔
  for (let r = 0; r < 9; r++) {
    if (r !== row && currentBoard[r][col] === value) return true;
  }
  // 3x3 서브 블록 내부 중복 수치 스캔
  const subRowStart = Math.floor(row / 3) * 3;
  const subColStart = Math.floor(col / 3) * 3;
  for (let r = subRowStart; r < subRowStart + 3; r++) {
    for (let c = subColStart; c < subColStart + 3; c++) {
      if ((r !== row || c !== col) && currentBoard[r][c] === value) {
        return true;
      }
    }
  }
  return false;
}

10. 게임 완료 처리 및 jQuery 4.0 모달 제어

판 위의 모든 격자가 채워져 있고 중복 오류가 없는 완벽한 상태를 검증한 후 성공을 감축하는 모달 윈도우를 노출하는 것으로 튜토리얼을 마무리합니다.

function evaluateGameSuccess() {
  // 빈 칸 잔존 여부 확인
  const hasEmptyCell = currentBoard.some(row => row.includes(0));
  // 오류 셀 탐색
  const hasConflict = $('.collision-alert').length > 0;

  if (!hasEmptyCell && !hasConflict) {
    // 모달 활성화를 위한 클래스 주입
    $('#game-modal').addClass('show');
  }
}

function resetBoardData() {
  currentBoard = JSON.parse(JSON.stringify(puzzleTemplate));
  selectedCell = { row: -1, col: -1 };
  renderBoard();
}