돌아가기
#wasm
#rust
#nextjs
#conway's life game

웹어셈블리로 콘웨이의 생명 게임 만들기 (with Rust)

콘웨이의 생명 게임이란

영국의 수학자 존 호른 콘웨이가 고안해낸 세포자동자 의 일종이다.

게임은 처음 입력된 초기값에 따라 진행이 완전히 결정되며, 각 세포 주위에 인접해있는 8개의 이웃 세포에 따라 다음 세대의 세포 상태를 결정하게 된다.

게임의 규칙

  1. 죽은 세포의 이웃 중 정확히 3개가 살아있으면 그 세포는 다음 세대에 살아난다.
  2. 살아있는 세포의 이웃 중 2개 또는 3개가 살아있으면 그 세포는 다음 세대에도 살아있게 된다.
  3. 이외의 경우, 그 세포는 다음 세대에 죽게 된다.

웹 어셈블리로 구현하기

러스트를 사용하여 코드를 구현 후 이를 웹어셈블리로 컴파일하여 웹 브라우저에서 실행할 수 있도록 한다.

wasm-bindgenwasm-pack을 사용하였다.

코드 작성

  1. 먼저 게임의 셀과 게임 화면을 정의한다.
rust
#[wasm_bindgen]
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
    Dead = 0,
    Alive = 1,
}
 
#[wasm_bindgen]
pub struct Universe {
    width: u32,
    height: u32,
    cells: Vec<Cell>,
}
  1. Universe 구조체를 구현한다.

먼저 게임 셀의 getter/setter 함수를 구현한다.

rust
#[wasm_bindgen]
impl Universe {
    pub fn width(&self) -> u32 {
        self.width
    }
 
    pub fn height(&self) -> u32 {
        self.height
    }
 
    pub fn cells(&self) -> *const Cell {
        self.cells.as_ptr()
    }
 
    fn get_index(&self, row: u32, column: u32) -> usize {
        (row * self.width + column) as usize
    }
 
    pub fn set_cell(&mut self, row: u32, column: u32, cell: Cell) {
        let idx = self.get_index(row, column);
        self.cells[idx] = cell;
    }
 
    // ...
}

그 다음 이웃 세포의 개수를 세는 함수를 구현한다.

rust
#[wasm_bindgen]
impl Universe {
    // ...
    fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
        let mut count = 0;
        for delta_row in [self.height - 1, 0, 1].iter().cloned() {
            for delta_col in [self.width - 1, 0, 1].iter().cloned() {
                if delta_row == 0 && delta_col == 0 {
                    continue;
                }
 
                let neighbor_row = (row + delta_row) % self.height;
                let neighbor_col = (column + delta_col) % self.width;
                let idx = self.get_index(neighbor_row, neighbor_col);
                count += self.cells[idx] as u8;
            }
        }
        count
    }
    // ...
}

이제 매 세대마다 세포의 상태를 업데이트하는 함수를 구현한다. 앞서 언급한 규칙을 적용하여 다음 세대의 세포 상태를 결정한다.

rust
#[wasm_bindgen]
impl Universe {
    // ...
    pub fn tick(&mut self) {
        let mut next = self.cells.clone();
 
        for row in 0..self.height {
            for col in 0..self.width {
                let idx = self.get_index(row, col);
                let cell = self.cells[idx];
                let live_neighbors = self.live_neighbor_count(row, col);
 
                let next_cell = match (cell, live_neighbors) {
                    // 규칙 1: 살아있는 세포의 이웃 중 2개 미만이면 죽는다.
                    (Cell::Alive, x) if x < 2 => Cell::Dead,
                    // 규칙 2: 살아있는 세포의 이웃 중 2개 또는 3개가 살아있으면 다음 세대에도 살아있게 된다.
                    (Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
                    // 규칙 3: 살아있는 세포의 이웃 중 3개 초과이면 죽는다.
                    (Cell::Alive, x) if x > 3 => Cell::Dead,
                    // 규칙 4: 죽은 세포의 이웃 중 정확히 3개가 살아있으면 다음 세대에 살아난다.
                    (Cell::Dead, 3) => Cell::Alive,
                    // 이외의 경우, 그 세포는 다음 세대에 죽게 된다.
                    (otherwise, _) => otherwise,
                };
 
                next[idx] = next_cell;
            }
        }
 
        self.cells = next;
    }
    // ...
}

마지막으로 게임 초기값을 설정하는 함수를 구현한다.

rust
#[wasm_bindgen]
impl Universe {
    // ...
    pub fn new(width: u32, height: u32) -> Universe {
        let cells = (0..width * height)
            .map(|i| Cell::Dead)
            .collect();
 
        Universe {
            width,
            height,
            cells,
        }
    }
}

웹 어셈블리로 컴파일하기

먼저 프로젝트 루트 디렉토리에서 다음 명령어를 실행하여 웹 어셈블리로 컴파일한다.

bash
wasm-pack build .
Terminal

컴파일 후 생성된 파일은 pkg 디렉토리에 생성된다.

Next.js에서 웹어셈블리 사용하기

웹어셈블리 모듈 로딩

Next.js 환경에서 웹어셈블리를 사용하기 위해서는 클라이언트 사이드에서만 실행되도록 해야 한다. dynamic import를 사용하여 SSR을 비활성화한다.

typescript
import dynamic from "next/dynamic";
 
const WasmConwaysGame = dynamic(() => Promise.resolve(WasmConwaysGameComponent), {
    ssr: false,
});

웹어셈블리 메모리 관리

웹어셈블리의 가장 중요한 특징 중 하나는 메모리 공유이다. 러스트에서 생성된 데이터는 웹어셈블리의 선형 메모리에 저장되며, 이를 자바스크립트에서 직접 접근할 수 있다.

1. 메모리 객체 가져오기

typescript
const [memory, setMemory] = useState<any | null>(null);
 
// 웹어셈블리 모듈과 메모리 로딩
const loadWasm = async () => {
    const wasmModule = await import("@/examples/conway-wasm/pkg/conway_wasm");
    const wasmMemoryModule = await import("@/examples/conway-wasm/pkg/conway_wasm_bg.wasm");
    
    setMemory(wasmMemoryModule.memory);
    // ...
};

2. 메모리에서 데이터 직접 읽기

웹어셈블리의 선형 메모리에서 데이터를 직접 읽어오는 것이 핵심이다.

typescript
// 러스트에서 셀 데이터의 포인터 가져오기
const cellsPtr = universe.cells();
 
// 메모리에서 직접 데이터 읽기 (포인터 기반)
const cells = new Uint8Array(memory.buffer, cellsPtr, width * height);

이 방식의 장점:

  • 성능: 자바스크립트와 웹어셈블리 간 데이터 복사 없음
  • 메모리 효율성: 동일한 메모리 공간을 공유
  • 실시간 동기화: 웹어셈블리에서 데이터가 변경되면 자바스크립트에서 즉시 반영

3. 메모리 레이아웃 이해

러스트의 Vec<Cell>은 웹어셈블리 메모리에서 다음과 같이 배치된다:

rust
// 러스트 코드
pub struct Universe {
    width: u32,      // 4바이트
    height: u32,     // 4바이트  
    cells: Vec<Cell> // 포인터 + 길이 (8바이트 + 8바이트)
}

자바스크립트에서는 이 구조를 다음과 같이 해석한다:

typescript
// 메모리에서 직접 구조체 데이터 읽기
const cellsPtr = universe.cells(); // Vec의 포인터
const cells = new Uint8Array(memory.buffer, cellsPtr, width * height);

4. 실시간 렌더링

메모리 공유를 통해 실시간으로 게임 상태를 렌더링할 수 있다:

typescript
useEffect(() => {
    if (!universe || !canvasRef.current || !Cell || !memory) return;
    
    const width = universe.width();
    const height = universe.height();
    const cellsPtr = universe.cells();
    const cells = new Uint8Array(memory.buffer, cellsPtr, width * height);
    
    // 캔버스에 직접 렌더링
    for (let row = 0; row < height; row++) {
        for (let col = 0; col < width; col++) {
            const idx = row * width + col;
            ctx.fillStyle = cells[idx] === Cell.Alive ? ALIVE_COLOR : DEAD_COLOR;
            ctx.fillRect(col * CELL_SIZE, row * CELL_SIZE, CELL_SIZE, CELL_SIZE);
        }
    }
}, [universe, generation, Cell, memory]);

5. 메모리 안전성 주의사항

웹어셈블리 메모리를 직접 조작할 때는 다음 사항을 주의해야 한다:

  • 포인터 유효성: 메모리 해제 후 포인터 사용 금지
  • 경계 검사: 배열 범위를 벗어나는 접근 방지
  • 동기화: 메모리 변경 시 적절한 상태 업데이트
typescript
// 안전한 메모리 접근 예시
const handleCanvasClick = (e: React.MouseEvent<HTMLCanvasElement>) => {
    if (!universe || !canvasRef.current || !Cell || !memory) return;
    
    // 좌표 검증
    const x = Math.floor((e.clientX - rect.left) * scaleX / CELL_SIZE);
    const y = Math.floor((e.clientY - rect.top) * scaleY / CELL_SIZE);
    
    if (x < 0 || y < 0 || x >= universe.width() || y >= universe.height()) return;
    
    // 메모리에서 안전하게 데이터 읽기
    const cellsPtr = universe.cells();
    const cells = new Uint8Array(memory.buffer, cellsPtr, universe.width() * universe.height());
    const idx = y * universe.width() + x;
    
    // 상태 변경
    const current = cells[idx];
    universe.set_cell(y, x, current === Cell.Alive ? Cell.Dead : Cell.Alive);
};

완성본

UI 컴포넌트 코드

tsx
'use client';
 
import dynamic from "next/dynamic";
import { useEffect, useRef, useState } from "react";
 
const CELL_SIZE = 10;
const GRID_COLOR = "#3f3f46";
const DEAD_COLOR = "#09090b";
const ALIVE_COLOR = "#a1a1aa";
 
// 특수 패턴들 정의
const PATTERNS = {
  glider: {
    name: "글라이더",
    cells: [
      [0, 1],
      [1, 2],
      [2, 0],
      [2, 1],
      [2, 2]
    ]
  },
  spaceshipSmall: {
    name: "소형 우주선",
    cells: [
      [0, 1],
      [0, 4],
      [1, 0],
      [2, 0],
      [2, 4],
      [3, 0],
      [3, 1],
      [3, 2],
      [3, 3]
    ]
  },
  gun: {
    name: "글라이더 총",
    cells: [
      [0, 24],
      [1, 22],
      [1, 24],
      [2, 12],
      [2, 13],
      [2, 20],
      [2, 21],
      [2, 34],
      [2, 35],
      [3, 11],
      [3, 15],
      [3, 20],
      [3, 21],
      [3, 34],
      [3, 35],
      [4, 0],
      [4, 1],
      [4, 10],
      [4, 16],
      [4, 20],
      [4, 21],
      [5, 0],
      [5, 1],
      [5, 10],
      [5, 14],
      [5, 16],
      [5, 17],
      [5, 22],
      [5, 24],
      [6, 10],
      [6, 16],
      [6, 24],
      [7, 11],
      [7, 15],
      [8, 12],
      [8, 13]
    ]
  },
  locomotive: {
    name: "기관차",
    cells: [
      [0, 2],
      [0, 3],
      [1, 0],
      [1, 1],
      [1, 3],
      [1, 4],
      [2, 0],
      [2, 1],
      [2, 2],
      [2, 3]
    ]
  },
  beacon: {
    name: "비콘",
    cells: [
      [0, 0],
      [0, 1],
      [1, 0],
      [2, 3],
      [3, 2],
      [3, 3]
    ]
  },
  toad: {
    name: "두꺼비",
    cells: [
      [1, 1],
      [1, 2],
      [1, 3],
      [2, 0],
      [2, 1],
      [2, 2]
    ]
  },
  acorn: {
    name: "도토리",
    cells: [
      [0, 1],
      [1, 3],
      [2, 0],
      [2, 1],
      [2, 4],
      [2, 5],
      [2, 6]
    ]
  },
  diehard: {
    name: "다이하드",
    cells: [
      [0, 6],
      [1, 0],
      [1, 1],
      [2, 1],
      [2, 5],
      [2, 6],
      [2, 7]
    ]
  }
};
 
function WasmConwaysGameComponent() {
    const [universe, setUniverse] = useState<any | null>(null);
    const [Cell, setCell] = useState<any | null>(null);
    const [memory, setMemory] = useState<any | null>(null);
    const [isLoading, setIsLoading] = useState(true);
    const [error, setError] = useState<string | null>(null);
    const [isRunning, setIsRunning] = useState(false);
    const [generation, setGeneration] = useState(0);
    const animationRef = useRef<number | null>(null);
    const canvasRef = useRef<HTMLCanvasElement>(null);
    const [dimensions, setDimensions] = useState({ width: 84, height: 54 });
    const [speed, setSpeed] = useState(60);
    const tickTimeoutRef = useRef<NodeJS.Timeout | null>(null);
    const [renderTrigger, setRenderTrigger] = useState(0);
 
    useEffect(() => {
        const loadWasm = async () => {
            try {
                setIsLoading(true);
                setError(null);
                const wasmModule = await import("@/examples/conway-wasm/pkg/conway_wasm");
                const wasmMemoryModule = await import("@/examples/conway-wasm/pkg/conway_wasm_bg.wasm");
                setCell(wasmModule.Cell);
                setMemory(wasmMemoryModule.memory);
                const uni = await wasmModule.Universe.new(dimensions.width, dimensions.height);
                setUniverse(uni);
                setGeneration(0);
            } catch (err) {
                setError(err instanceof Error ? err.message : '알 수 없는 오류가 발생했습니다.');
            } finally {
                setIsLoading(false);
            }
        };
        loadWasm();
    }, [dimensions.width, dimensions.height]);
 
    // 캔버스 렌더링
    useEffect(() => {
        if (!universe || !canvasRef.current || !Cell || !memory) return;
        const width = universe.width();
        const height = universe.height();
        const cellsPtr = universe.cells();
        const cells = new Uint8Array(memory.buffer, cellsPtr, width * height);
        const ctx = canvasRef.current.getContext("2d");
        if (!ctx) return;
        
        ctx.imageSmoothingEnabled = false;
        
        ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
        
        // 배경
        ctx.fillStyle = DEAD_COLOR;
        ctx.fillRect(0, 0, canvasRef.current.width, canvasRef.current.height);
        
        // 셀 먼저 그리기
        for (let row = 0; row < height; row++) {
            for (let col = 0; col < width; col++) {
                const idx = row * width + col;
                ctx.fillStyle = cells[idx] === Cell.Alive ? ALIVE_COLOR : DEAD_COLOR;
                ctx.fillRect(
                    col * CELL_SIZE,
                    row * CELL_SIZE,
                    CELL_SIZE,
                    CELL_SIZE
                );
            }
        }
        
        // 그리드 선
        ctx.strokeStyle = GRID_COLOR;
        ctx.lineWidth = 0.2;
        
        // 수평선
        for (let y = 0; y <= height; y++) {
            const yPos = y * CELL_SIZE;
            ctx.beginPath();
            ctx.moveTo(0, yPos);
            ctx.lineTo(width * CELL_SIZE, yPos);
            ctx.stroke();
        }
        
        // 수직선
        for (let x = 0; x <= width; x++) {
            const xPos = x * CELL_SIZE;
            ctx.beginPath();
            ctx.moveTo(xPos, 0);
            ctx.lineTo(xPos, height * CELL_SIZE);
            ctx.stroke();
        }
    }, [universe, generation, Cell, memory, renderTrigger]);
 
    // 애니메이션 루프
    useEffect(() => {
        if (!isRunning) {
            if (tickTimeoutRef.current) {
                clearTimeout(tickTimeoutRef.current);
                tickTimeoutRef.current = null;
            }
            return;
        }
        const step = () => {
            if (universe) {
                universe.tick();
                setGeneration((g) => g + 1);
            }
            tickTimeoutRef.current = setTimeout(step, speed);
        };
        tickTimeoutRef.current = setTimeout(step, speed);
        return () => {
            if (tickTimeoutRef.current) {
                clearTimeout(tickTimeoutRef.current);
                tickTimeoutRef.current = null;
            }
        };
    }, [isRunning, universe, speed]);
 
    // 셀 클릭 토글
    const handleCanvasClick = (e: React.MouseEvent<HTMLCanvasElement>) => {
        if (!universe || !canvasRef.current || !Cell || !memory) return;
        const rect = canvasRef.current.getBoundingClientRect();
        const scaleX = canvasRef.current.width / rect.width;
        const scaleY = canvasRef.current.height / rect.height;
        const x = Math.floor((e.clientX - rect.left) * scaleX / CELL_SIZE);
        const y = Math.floor((e.clientY - rect.top) * scaleY / CELL_SIZE);
        if (x < 0 || y < 0 || x >= universe.width() || y >= universe.height()) return;
        const cellsPtr = universe.cells();
        const cells = new Uint8Array(memory.buffer, cellsPtr, universe.width() * universe.height());
        const idx = y * universe.width() + x;
        const current = cells[idx];
        universe.set_cell(y, x, current === Cell.Alive ? Cell.Dead : Cell.Alive);
        setGeneration((g) => g + 1);
    };
 
    // 컨트롤 버튼
    const handleStart = () => setIsRunning(true);
    const handlePause = () => setIsRunning(false);
    const handleReset = async () => {
        setIsRunning(false);
        setIsLoading(true);
        setError(null);
        try {
            const wasmModule = await import("@/examples/conway-wasm/pkg/conway_wasm");
            const uni = await wasmModule.Universe.new(dimensions.width, dimensions.height);
            setUniverse(uni);
            setGeneration(0);
        } catch (err) {
            setError(err instanceof Error ? err.message : '알 수 없는 오류가 발생했습니다.');
        } finally {
            setIsLoading(false);
        }
    };
 
    // 다음 틱
    const handleNextTick = () => {
        if (universe) {
            universe.tick();
            setGeneration((g) => g + 1);
        }
    };
 
    // 패턴 삽입 함수
    const insertPattern = (patternKey: keyof typeof PATTERNS) => {
        if (!universe || !Cell) return;
        
        const pattern = PATTERNS[patternKey];
        const width = universe.width();
        const height = universe.height();
        
        // 먼저 모든 셀을 죽음 상태로 초기화
        for (let row = 0; row < height; row++) {
            for (let col = 0; col < width; col++) {
                universe.set_cell(row, col, Cell.Dead);
            }
        }
        
        // 중앙에 패턴 배치
        const centerX = Math.floor(width / 2);
        const centerY = Math.floor(height / 2);
        
        // 패턴 크기 계산
        const maxRow = Math.max(...pattern.cells.map(cell => cell[0]));
        const maxCol = Math.max(...pattern.cells.map(cell => cell[1]));
        const minRow = Math.min(...pattern.cells.map(cell => cell[0]));
        const minCol = Math.min(...pattern.cells.map(cell => cell[1]));
        
        const patternWidth = maxCol - minCol + 1;
        const patternHeight = maxRow - minRow + 1;
        
        // 패턴 시작 위치
        const startX = centerX - Math.floor(patternWidth / 2);
        const startY = centerY - Math.floor(patternHeight / 2);
        
        // 패턴 셀들을 유니버스에 설정
        pattern.cells.forEach(([row, col]) => {
            const x = startX + col - minCol;
            const y = startY + row - minRow;
            
            // 경계 체크
            if (x >= 0 && x < width && y >= 0 && y < height) {
                universe.set_cell(y, x, Cell.Alive);
            }
        });
        
        setGeneration(0);
        setRenderTrigger(prev => prev + 1);
    };
 
    return (
      <div className="bg-zinc-950 text-zinc-100 p-4">
        <div className="max-w-4xl mx-auto">
          {/* 컨트롤 패널 */}
          <div className="bg-zinc-900/50 rounded-lg p-4 mb-4 border border-zinc-800/50">
            <div className="flex flex-wrap gap-2 justify-between items-center">
              <div className="flex gap-2">
                <button
                  className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
                    isRunning
                      ? "bg-zinc-600 hover:bg-zinc-500 text-white"
                      : "bg-zinc-700 hover:bg-zinc-600 text-zinc-100"
                  } disabled:opacity-50 disabled:cursor-not-allowed`}
                  onClick={isRunning ? handlePause : handleStart}
                  disabled={isLoading || !!error}
                >
                  {isRunning ? "일시정지" : "시작"}
                </button>
                <button
                  className="px-3 py-2 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-200 text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
                  onClick={handleReset}
                  disabled={isLoading}
                >
                  초기화
                </button>
                <button
                  className="px-3 py-2 rounded-lg bg-zinc-600 hover:bg-zinc-500 text-white text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
                  onClick={handleNextTick}
                  disabled={isRunning || isLoading}
                >
                  다음 세대
                </button>
              </div>
 
              {/* 속도 컨트롤 */}
              <div className="flex items-center justify-center gap-3">
                <span className="text-zinc-400 text-sm">속도</span>
                <div className="flex items-center gap-2 bg-zinc-800/30 px-3 py-1 rounded">
                  <span className="text-zinc-500 text-xs">빠름</span>
                  <input
                    type="range"
                    min={20}
                    max={500}
                    step={10}
                    value={speed}
                    onChange={(e) => setSpeed(Number(e.target.value))}
                    className="w-24 h-1 bg-zinc-700 rounded appearance-none cursor-pointer slider"
                    style={{
                      background: `linear-gradient(to right, #a1a1aa 0%, #a1a1aa ${
                        ((speed - 20) / 480) * 100
                      }%, #52525b ${
                        ((speed - 20) / 480) * 100
                      }%, #52525b 100%)`,
                    }}
                  />
                   <span className="text-zinc-500 text-xs">느림</span>
                </div>
                <div className="text-zinc-300 text-xs font-mono bg-zinc-800/30 px-2 py-1 rounded">
                  {(1000 / speed).toFixed(1)} gen/s
                </div>
              </div>
 
              {/* 세대 정보 */}
              <div className="flex items-center gap-2 bg-zinc-800/50 px-3 py-1 rounded border border-zinc-700/50">
                <span className="text-zinc-400 text-sm">세대:</span>
                <span className="text-zinc-100 font-mono font-semibold">
                  {generation.toLocaleString()}
                </span>
              </div>
            </div>
          </div>
 
          {/* 패턴 툴바 */}
          <div className="bg-zinc-900/30 rounded-lg p-3 mb-4 border border-zinc-800/30">
            <div className="flex flex-wrap gap-2 items-center">
              <span className="text-zinc-400 text-sm font-medium mr-2">패턴:</span>
              {Object.entries(PATTERNS).map(([key, pattern]) => (
                <button
                  key={key}
                  className="px-3 py-1.5 rounded-md bg-zinc-800/50 hover:bg-zinc-700/70 text-zinc-200 text-xs font-medium transition-colors border border-zinc-700/30 hover:border-zinc-600/50 disabled:opacity-50 disabled:cursor-not-allowed"
                  onClick={() => insertPattern(key as keyof typeof PATTERNS)}
                  disabled={isLoading || !!error}
                  title={`${pattern.name} 패턴을 중앙에 삽입합니다`}
                >
                  {pattern.name}
                </button>
              ))}
              <div className="ml-auto text-zinc-500 text-xs">
                클릭하여 패턴을 중앙에 삽입
              </div>
            </div>
          </div>
 
          {/* 게임 보드 */}
          <div className="flex justify-center">
            <div className="relative">
              <div
                className="rounded-lg border border-zinc-800/50 overflow-hidden bg-zinc-950"
                style={{
                  background: `linear-gradient(135deg, ${DEAD_COLOR} 0%, #18181b 100%)`,
                }}
              >
                <canvas
                  ref={canvasRef}
                  width={dimensions.width * CELL_SIZE}
                  height={dimensions.height * CELL_SIZE}
                  onClick={handleCanvasClick}
                  className="block cursor-crosshair select-none"
                  style={{
                    imageRendering: "pixelated",
                    background: DEAD_COLOR,
                    maxWidth: "100%",
                    height: "auto",
                  }}
                />
              </div>
 
              {/* 인터랙션 힌트 */}
              {!isRunning && !isLoading && (
                <div className="absolute -bottom-8 left-1/2 transform -translate-x-1/2 text-center">
                  <p className="text-zinc-500 text-xs">
                    클릭하여 셀을 토글할 수 있습니다
                  </p>
                </div>
              )}
            </div>
          </div>
 
          {/* 로딩 및 에러 상태 */}
          {isLoading && (
            <div className="text-center mt-4">
              <div className="inline-flex items-center gap-2 bg-zinc-800/50 px-4 py-2 rounded">
                <div className="w-4 h-4 border-2 border-zinc-600 border-t-zinc-400 rounded-full animate-spin"></div>
                <span className="text-zinc-300 text-sm">
                  WebAssembly 모듈을 로딩 중...
                </span>
              </div>
            </div>
          )}
 
          {error && (
            <div className="text-center mt-4">
              <div className="inline-flex items-center gap-2 bg-red-950/50 px-4 py-2 rounded border border-red-900/50">
                <span className="text-red-400 text-sm">{error}</span>
              </div>
            </div>
          )}
        </div>
 
        <style jsx>{`
          .slider::-webkit-slider-thumb {
            appearance: none;
            height: 12px;
            width: 12px;
            border-radius: 50%;
            background: #a1a1aa;
            cursor: pointer;
            box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
            transition: all 0.2s ease;
          }
          .slider::-webkit-slider-thumb:hover {
            background: #d4d4d8;
            transform: scale(1.1);
          }
          .slider::-moz-range-thumb {
            height: 12px;
            width: 12px;
            border-radius: 50%;
            background: #a1a1aa;
            cursor: pointer;
            border: none;
            box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
          }
        `}</style>
      </div>
    );
}
 
const WasmConwaysGame = dynamic(() => Promise.resolve(WasmConwaysGameComponent), {
    ssr: false,
});
 
export default WasmConwaysGame;