WebGL 실무 정리 — 브라우저에서 GPU를 직접 부리는 법

OpenGL ES 기반 WebGL의 셰이더 파이프라인, 리소스 관리, 컨텍스트 손실 대응을 실무 체크리스트와 예제 코드로 정리한다

2026-08-12 · 최초 발행 2025-12-09

브라우저에서 3D 지도를 돌리거나 수십만 개의 산포도 점을 실시간으로 그리는 화면은 대부분 WebGL 위에서 돌아간다. OpenGL ES 기반의 로우레벨 JavaScript API인 WebGL(Web Graphics Library)은 HTML5 Canvas에 GPU 렌더링 컨텍스트를 부여해 웹에서 2D/3D 그래픽을 고성능으로 구현하는 표준 기술이다. OpenGL ES 2.0/3.0과 GLSL ES를 바탕으로 셰이더 중심 파이프라인을 제공한다.

브라우저가 GPU에 접근하는 경로

WebGL은 OpenGL ES 기능을 노출하는 JavaScript 바인딩이다. WebGL 1.0은 OpenGL ES 2.0에, WebGL 2.0은 OpenGL ES 3.0에 대응한다. HTMLCanvasElement에서 WebGLRenderingContextWebGL2RenderingContext를 획득해 사용하며, GLSL ES 셰이더 언어와 타입드 배열로 GPU 리소스를 관리한다. 렌더링 파이프라인은 버텍스 처리 → 래스터화 → 프래그먼트 처리 → 프레임버퍼 출력 순으로 진행되는 상태 머신 기반 API이고, 드로우 콜 중심으로 렌더링 루프를 구성한다.

컨텍스트, 셰이더, 그리고 GPU 리소스

canvas.getContext('webgl' | 'webgl2')로 컨텍스트를 얻고 나면, 컨텍스트 손실·복구 이벤트에 대응하는 구조를 미리 짜둬야 한다. OES_vertex_array_object, ANGLE_instanced_arrays, EXT_color_buffer_float, WEBGL_debug_renderer_info 같은 확장으로 기능을 넓히고, getParameter·getExtension·getSupportedExtensions로 실행 가능한 기능을 그때그때 확인한다.

셰이더는 GLSL ES로 버텍스·프래그먼트 셰이더를 작성해 컴파일하고 프로그램으로 링크한다. compileStatus/linkStatus로 에러를 진단하고 KHR_debug로 메시지 콜백을 받을 수 있다. 유니폼·UBO, define 기반 셰이더 분기, 캐시를 활용하면 셰이더 전환 비용을 줄일 수 있다.

버퍼는 ArrayBuffer(ElementArrayBuffer)와 VAO(WebGL2 또는 확장)로 바인딩 오버헤드를 줄인다. 텍스처는 NPOT을 지원하고 mipmap·필터링·랩 모드를 설정하며, ETC/ASTC/BC 같은 압축 포맷으로 대역폭을 아낄 수 있다. 프레임버퍼는 오프스크린 렌더링이나 그림자·후처리 같은 멀티패스에 쓰이고 컬러·깊이·스텐실 어태치를 관리한다.

렌더링 루프와 리소스 수명

requestAnimationFrame 기반으로 프레임을 스케줄링하고, 고정 타임스텝과 가변 타임스텝 중 전략을 선택한다. 드로우 콜을 최소화하고 상태 변경을 배치로 묶고 인스턴싱을 쓰면 CPU 오버헤드가 줄어든다. WebGL2의 fence sync나 timer query(브라우저·플랫폼별 차이 존재)로 동기화하고, 픽셀 리드백은 비동기 처리를 고려해야 한다.

텍스처·버퍼는 수명 주기를 관리하고 delete 호출로 누수를 막아야 한다. 대규모 데이터는 스트리밍이나 부분 업데이트(orphan + subData)로 처리하고, LRU 캐시로 VRAM 사용량을 제어하며, 컨텍스트 손실에 대비해 재생성 로직을 준비해둔다. 크로스 오리진 자원은 CORS 설정과 crossOrigin 속성이 필요하다.

최소 삼각형 렌더링 예제

최신 브라우저(Chrome/Firefox/Edge)에서 WebGL 1.0과 GLSL ES 1.00으로 동작하며, 로컬 환경이라면 HTTPS 제공을 권장한다. 순수 WebGL(바닐라) 구현이고, 복잡한 장면은 Three.js·regl·twgl.js 같은 라이브러리를 고려할 만하다.

<canvas id="gl" width="640" height="360"></canvas>
<script>
const canvas = document.getElementById('gl');
const gl = canvas.getContext('webgl', {antialias: true});
if (!gl) throw new Error('WebGL not supported');

const vs = `
attribute vec2 a_pos;
void main() {
  gl_Position = vec4(a_pos, 0.0, 1.0);
}`;
const fs = `
precision mediump float;
void main() {
  gl_FragColor = vec4(0.1, 0.6, 0.9, 1.0);
}`;

function compile(type, src) {
  const sh = gl.createShader(type);
  gl.shaderSource(sh, src);
  gl.compileShader(sh);
  if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
    const log = gl.getShaderInfoLog(sh);
    gl.deleteShader(sh);
    throw new Error('Shader compile error: ' + log);
  }
  return sh;
}

const prog = gl.createProgram();
gl.attachShader(prog, compile(gl.VERTEX_SHADER, vs));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, fs));
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
  throw new Error('Program link error: ' + gl.getProgramInfoLog(prog));
}
gl.useProgram(prog);

const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
// 삼각형 3점(정규화 클립 좌표)
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
  0,  0.8,
 -0.8, -0.8,
  0.8, -0.8
]), gl.STATIC_DRAW);

const loc = gl.getAttribLocation(prog, 'a_pos');
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);

gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0.02, 0.02, 0.05, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
</script>

데이터가 GPU까지 흐르는 경로

컨텍스트생성('webgl'/'webgl2')리소스업로드('Buffer'/'Texture'/'Framebuffer')셰이더 컴파일/링크셰이더 컴파일/링크 실패?드로우콜('gl.drawArrays'/'gl.drawElements')렌더 타깃화면갱신('requestAnimationFrame')컨텍스트 손실 이벤트?버퍼/텍스처/프로그램재업로드HTML 'canvas' 요소WebGLRenderingContext/WebGL2RenderingContextGPU 메모리프로그램 객체오류 로깅 복구('getError','KHR_debug')GPU파이프라인('Vertex'→'Raster'→'Fragment')프레임버퍼('Default'/'Offscreen')디스플레이자원 재생성 흐름('restore')

실무 체크리스트

성능 측면에서는 배칭·VAO·인스턴싱(WebGL2 또는 확장)으로 드로우 콜과 상태 변경을 최소화하고, 텍스처 아틀라스·mipmap·적절한 필터링·압축 포맷(ETC/ASTC/BC)을 쓰며, DYNAMIC_DRAW + orphaning(gl.bufferData null) + gl.bufferSubData로 데이터 전송을 스트리밍 최적화한다.

품질·일관성 측면에서는 sRGB 감마 관리, precision 명시, 깊이·스텐실·블렌드 설정 검증이 필요하고, 기기별 차이는 기능 쿼리와 폴백 경로로 대응하며 동일성 테스트 스냅샷을 도입할 만하다.

안정성·보안 측면에서는 webglcontextlost/webglcontextrestored 이벤트 핸들러를 반드시 구현해야 한다. 교차 출처 이미지·비디오는 CORS 헤더와 crossOrigin 설정이 필요하고, WebXR·센서 API는 HTTPS 같은 안전한 컨텍스트를 요구한다. 디버깅은 KHR_debug를 쓰고 getError 폴링은 최소화하며, 성능 분석은 브라우저 GPU 프로파일러를 활용한다.

운영·배포 측면에서는 glTF + Draco/KTX2(압축 텍스처)로 에셋 파이프라인의 다운로드·디코딩 비용을 줄이고, 워커나 OffscreenCanvas로 메인 스레드 차단을 최소화하되 폴백 경로는 유지한다. 라이브러리는 Three.js의 생산성과 regl/twgl의 미세 제어 사이에서 요구사항에 맞춰 트레이드오프를 명시적으로 판단해야 한다.

Canvas 2D, WebGL, WebGPU 비교

항목 Canvas 2D WebGL (1/2) WebGPU
성능 CPU 기반, 단일 스레드 병목 GPU 가속, 대규모 지오메트리/파이프라인 차세대 저수준, 높은 스루풋
확장성 제한적 후처리/합성 확장/프레임버퍼/인스턴싱/멀티패스 멀티큐/컴퓨트 풍부
일관성 브라우저간 차이 적음 드라이버/디바이스 차이 대비 필요 표준화 진행 중, 최신 정보 확인 필요
안정성 단순, 리소스 적음 컨텍스트 손실 처리 필요 샌드박스·검증 강화
운영 편의 진입 장벽 낮음 셰이더/상태 관리 부담 툴체인/러닝커브 큼

WebGPU 지원 범위는 브라우저·플랫폼에 따라 상이하므로 최신 정보를 확인해야 한다. 동일 장면 기준으로 WebGL은 Canvas 2D 대비 550배 처리량 개선이 가능하며(드로우 콜·픽셀 필률·기기 스펙에 의존), 중급 GPU 환경에서 100K1M 트라이앵글/프레임으로 60FPS를 달성한 사례도 있다.

glTF PBR 뷰어를 붙이는 절차

glTF 2.0 + KTX2 텍스처 + Draco 압축 메시를 입력으로 받아, 에셋 로더가 Web Worker에서 디코딩한 뒤 VBO/IBO/텍스처로 GPU에 업로드하고 IBL/BRDF LUT 같은 셰이더 파라미터를 바인딩한다. 이후 딜레이드나 포워드 렌더링을 거쳐 TAA/Bloom 같은 후처리를 적용하고 캔버스 프레임버퍼로 출력한다. 확장이 지원되지 않으면 단순 머티리얼로 폴백하고, 메모리를 초과하면 LOD나 스트리밍으로 전환한다. sRGB 정합과 장치 픽셀 비율(DPR) 반영, 리사이즈 시 뷰포트·프레임버퍼 재생성도 함께 처리해야 일관성이 유지된다.

활용 사례

glTF 기반 PBR 머티리얼로 CAD·PLM 웹 뷰어를 구현하는 제품 뷰어·설계 검토, Mapbox GL JS·deck.gl로 대규모 벡터·포인트 클라우드를 렌더링하는 지도·지리 시각화, 수십만 점의 산포도나 네트워크 그래프·볼륨 렌더링을 다루는 데이터 시각화·BI, Three.js·Babylon.js 기반 3D 게임과 WebXR 연동(보안 맥락 필요), WebRTC/WebCodecs와 WebGL 셰이더를 결합한 실시간 영상 필터·합성 파이프라인까지 적용 범위가 넓다.

신규 프로젝트라면 WebGL2를 우선 채택하고, 자산 파이프라인(glTF/KTX2)을 표준화하며, 성능·품질 자동화 테스트 체계를 갖추는 편이 낫다.

WebGLGPU렌더링셰이더웹그래픽glTF