웹 프론트엔드 개발 연습: 설치 없는 스도쿠 게임 JavaScript 코드 리뷰

들어가며: 직접 만든 스도쿠 게임, 그 속이 궁금하다면?

안녕하세요! 최근 studyformyself에 방문자 분들이 머리를 식히며 즐기실 수 있도록 별도의 설치가 필요 없는 웹 기반 스도쿠 게임을 추가했습니다. 단순히 게임을 즐기는 것도 좋지만, 프로그래밍에 관심이 있으신 분들이라면 “이 81개의 칸에 숫자를 채우고 정답을 검증하는 원리가 뭘까?” 하는 호기심이 생기실 텐데요. 그래서 오늘은 이 스도쿠 게임이 어떤 원리로 동작하는지, 프론트엔드(HTML, CSS, JavaScript) 코드를 블록별로 나누어 상세히 분석해 보려고 합니다.

🎮 코드를 살펴보기 전에 먼저 게임을 플레이해보고 싶으시다면?
👉 studyformyself 스도쿠 게임 플레이하기에서 직접 두뇌 트레이닝을 경험해 보세요!


1. 핵심 알고리즘: 백트래킹으로 스도쿠 퍼즐 생성하기

스도쿠 게임의 가장 큰 숙제는 ‘매번 새로운 정답판을 어떻게 만들어낼 것인가’입니다. 이 게임은 고정된 퍼즐을 불러오는 것이 아니라, 게임을 시작할 때마다 자바스크립트가 무한대로 새로운 퍼즐을 생성합니다.

function generateSudoku(difficulty) {
    solution = Array.from({length: 9}, () => Array(9).fill(0));
    fillDiagonal(); // 대각선 3x3 박스 3개를 먼저 채움
    solveSudoku(solution); // 백트래킹으로 나머지 빈칸 완성
    
    puzzle = solution.map(row => [...row]);
    let removeCount = difficulty === 'easy' ? 30 : difficulty === 'medium' ? 45 : 55;
    
    while(removeCount > 0) {
      let r = Math.floor(Math.random() * 9);
      let c = Math.floor(Math.random() * 9);
      if(puzzle[r][c] !== 0) {
        puzzle[r][c] = 0; // 완성된 정답판에서 난이도에 맞게 구멍 뚫기
        removeCount--;
      }
    }
  }

  function solveSudoku(board) {
    for(let i = 0; i < 9; i++) {
      for(let j = 0; j < 9; j++) {
        if(board[i][j] === 0) {
          for(let c = 1; c <= 9; c++) {
            if(isSafe(board, i, j, c)) { // 가로, 세로, 3x3 구역 규칙 검사
              board[i][j] = c;
              if(solveSudoku(board)) return true; // 재귀 호출
              else board[i][j] = 0; // 막히면 다시 0으로 되돌림 (백트래킹)
            }
          }
          return false;
        }
      }
    }
    return true;
  }

[코드 설명]
이 게임의 두뇌 역할을 하는 ‘백트래킹(Backtracking)’ 알고리즘입니다. 먼저 fillDiagonal() 함수로 서로 간섭하지 않는 대각선 방향의 3×3 박스 세 개를 무작위 숫자로 채웁니다. 이후 solveSudoku() 함수가 실행되어 빈칸에 1부터 9까지의 숫자를 하나씩 넣어보고 규칙(isSafe)에 맞는지 검사합니다. 규칙에 맞으면 다음 칸으로 넘어가고(재귀 호출), 만약 진행하다가 더 이상 어떤 숫자도 넣을 수 없는 ‘막다른 길’에 다다르면 이전 단계로 돌아가서(board[i][j] = 0) 다른 숫자를 시도합니다. 이렇게 완성된 완벽한 해답지(solution)에서 난이도에 따라 일정 개수의 칸을 0으로 비워내면 플레이어가 풀게 될 puzzle이 완성됩니다.

2. DOM 조작: 81개의 스도쿠 보드 화면에 그리기

function renderBoard() {
    boardEl.innerHTML = '';
    for(let r = 0; r < 9; r++) {
      for(let c = 0; c < 9; c++) {
        const cell = document.createElement('div');
        cell.className = 'sudoku-cell';
        cell.tabIndex = 0;
        cell.dataset.r = r; cell.dataset.c = c; // 행과 열 데이터 저장
        
        // 연필(메모) 모드용 3x3 내부 그리드 생성
        const notesDiv = document.createElement('div');
        notesDiv.className = 'cell-notes';
        for(let i=1; i<=9; i++) notesDiv.innerHTML += `<span id="note-${r}-${c}-${i}"></span>`;
        cell.appendChild(notesDiv);
        
        // 메인 숫자 영역
        const valDiv = document.createElement('div');
        valDiv.className = 'cell-value';
        
        if(puzzle[r][c] !== 0) {
          valDiv.innerText = puzzle[r][c];
          cell.classList.add('fixed'); // 처음부터 주어진 숫자는 수정 불가 처리
        }
        cell.appendChild(valDiv);
        
        cell.addEventListener('click', () => {
          if(isPaused) return; // 일시정지 중 클릭 방지
          selectCell(cell);
        });
        boardEl.appendChild(cell);
      }
    }
  }

[코드 설명]
HTML에 81개의 <div>를 일일이 적는 대신, 자바스크립트의 document.createElement를 활용해 동적으로 보드를 렌더링합니다. 이 코드의 핵심은 하나의 칸(sudoku-cell) 안에 두 개의 레이어를 겹쳐 놓은 것입니다. 하나는 작은 숫자를 메모할 수 있는 9칸짜리 notesDiv이고, 다른 하나는 실제 정답이 들어갈 valDiv입니다. 초기 퍼즐 데이터(puzzle)에 숫자가 있다면 화면에 표시하고 .fixed 클래스를 부여하여 게임 도중 지워지거나 수정되지 않도록 고정합니다.

3. 상태 관리: 스택(Stack)을 활용한 완벽한 되돌리기(Undo)

function captureCellState(cell) {
    return {
      cell: cell,
      val: cell.querySelector('.cell-value').innerText,
      classes: cell.className,
      notes: Array.from(cell.querySelectorAll('.cell-notes span')).map(s => s.innerText)
    };
  }

  // 되돌리기 버튼 클릭 이벤트
  document.getElementById('btn-undo').addEventListener('click', () => {
    if(isPaused || history.length === 0) return;
    const lastState = history.pop(); // 배열의 마지막 상태를 꺼냄
    const cell = lastState.cell;
    
    // 클래스, 메인 숫자, 메모 상태 복구
    cell.className = lastState.classes;
    cell.querySelector('.cell-value').innerText = lastState.val;
    
    const noteSpans = cell.querySelectorAll('.cell-notes span');
    lastState.notes.forEach((note, idx) => noteSpans[idx].innerText = note);
    
    // 복구된 메인 숫자가 비어있으면 다시 메모 그리드를 표시
    cell.querySelector('.cell-notes').style.display = lastState.val === '' ? 'grid' : 'none';
    selectCell(cell);
  });

[코드 설명]
사용자가 입력한 숫자를 취소할 수 있는 ‘되돌리기’ 기능은 프로그래밍의 기본 자료구조인 **스택(Stack, LIFO)**을 응용하여 구현했습니다. 사용자가 보드에 어떤 입력(숫자 쓰기, 메모하기, 지우기 등)을 하기 직전에 captureCellState 함수가 호출되어 해당 칸의 모든 상태(색상 클래스, 메인 숫자, 9칸의 메모 기록)를 객체 형태로 묶어 history 배열에 저장(push)합니다. 되돌리기 버튼을 누르면 배열에서 가장 마지막에 저장된 데이터를 꺼내와(pop) 화면을 과거 상태로 완벽하게 복구합니다.

4. 로직 처리: 사용자 입력과 정답 검증 시스템

function handleInput(num) {
    const r = parseInt(selectedCell.dataset.r);
    const c = parseInt(selectedCell.dataset.c);
    const valDiv = selectedCell.querySelector('.cell-value');

    history.push(captureCellState(selectedCell)); // 상태 백업

    // 연필(메모) 모드일 경우의 처리
    if(pencilMode) {
      if(valDiv.innerText !== '') return; 
      const noteSpan = document.getElementById(`note-${r}-${c}-${num}`);
      noteSpan.innerText = noteSpan.innerText === '' ? num : ''; // 토글 방식
      return;
    }

    // 일반 모드일 경우 정답 확인
    valDiv.innerText = num;
    selectedCell.classList.remove('wrong-overlay');

    if(num === solution[r][c]) {
      selectedCell.classList.add('correct-overlay'); // 정답 시 초록색
      selectedCell.querySelector('.cell-notes').style.display = 'none'; // 메모 숨김
      if(checkWinCondition()) setTimeout(gameWin, 100);
    } else {
      selectedCell.classList.add('wrong-overlay'); // 오답 시 빨간색
      mistakes++;
      mistakeCntEl.innerText = mistakes;
      if(mistakes >= 3) gameOver(); // 실수 3번 누적 시 게임 오버
    }
  }

[코드 설명]
키보드나 화면의 숫자 패드를 눌렀을 때 실행되는 핵심 비즈니스 로직입니다. 먼저 pencilMode 변수가 true인지 확인하여, 연필 모드라면 작은 메모 영역의 숫자를 껐다 켜는(토글) 역할만 수행하고 종료합니다. 일반 모드라면 입력받은 숫자를 처음에 생성해둔 완벽한 해답지(solution[r][c])와 대조합니다. 일치할 경우 초록색 배경(.correct-overlay)을 적용하고 모든 칸이 채워졌는지 승리 조건을 검사합니다. 만약 틀렸다면 빨간색 배경(.wrong-overlay)을 적용하고 실수(Mistakes) 카운트를 올립니다.

5. 치팅 방지와 편의성: 일시정지 및 힌트 기능

// 일시정지 기능
  pauseBtn.addEventListener('click', () => {
    isPaused = !isPaused;
    pauseBtn.innerText = isPaused ? '▶' : '⏸';
    
    if(isPaused) {
      boardEl.classList.add('paused'); // CSS를 통해 보드 내 숫자 숨김
      if(selectedCell) {
        selectedCell.classList.remove('selected');
        selectedCell = null;
      }
    } else {
      boardEl.classList.remove('paused');
    }
  });

  // 힌트 기능
  hintBtn.addEventListener('click', () => {
    // ...조건 검사 생략...
    history.push(captureCellState(selectedCell));

    hints--; // 힌트 횟수 차감
    badgeEl.innerText = hints;
    
    if(hints === 0) hintBtn.classList.add('hint-empty'); // 힌트 소진 시 버튼 색상 변경

    // 정답 강제 주입
    selectedCell.querySelector('.cell-value').innerText = solution[r][c];
    selectedCell.classList.remove('wrong-overlay');
    selectedCell.classList.add('correct-overlay');
    selectedCell.querySelector('.cell-notes').style.display = 'none';

    if(checkWinCondition()) setTimeout(gameWin, 100);
  });

[코드 설명]
게임의 완성도를 높이는 디테일한 편의 기능들입니다. 타이머가 멈추는 일시정지 기능을 만들 때는 플레이어가 멈춰놓고 화면을 보며 고민하는 ‘치팅’을 방지해야 합니다. 따라서 isPaused 상태가 되면 보드에 .paused 클래스를 추가해 CSS의 opacity: 0 속성으로 보드의 모든 숫자를 투명하게 숨기도록 설계했습니다. 힌트 기능은 복잡한 연산 없이, 사용자가 선택한 칸에 백그라운드에 숨겨진 solution 배열의 정답을 그대로 복사하여 강제로 입력해 주는 직관적인 방식으로 구현되었습니다.

마치며

이렇게 프론트엔드 언어(HTML/CSS/JS)만을 이용해 완성도 높은 논리 퍼즐 게임을 만들어 보았습니다. 알고리즘(백트래킹), DOM 조작, 스택을 이용한 상태 관리 등 자바스크립트의 다양한 핵심 개념이 스도쿠 게임 하나에 모두 녹아있습니다.

코드가 어떻게 동작하는지 꼼꼼히 살펴보셨다면, 이제 직접 두뇌를 회전하며 플레이해 볼 차례입니다. 지금 바로 게임에 도전해 보세요!
👉 studyformyself 무료 스도쿠 게임 도전하기

웹 게임 개발이나 알고리즘 로직에 대해 더 궁금한 점이 있으시다면 언제든 댓글로 남겨주세요. 감사합니다!

1 thought on “웹 프론트엔드 개발 연습: 설치 없는 스도쿠 게임 JavaScript 코드 리뷰”

Leave a Comment