무료 포토샵 개발기 1편: GitHub 코드와 AI로 레이어·마스크 연결하기
무료 포토샵을 만들면서 모든 것을 처음부터 작성하지는 않았다. GitHub에서 개발되는 React와 PixiJS를 가져다 쓰고, AI와 함께 포토샷의 문서 구조·레이어·마스크를 연결하는 코드를 작성했다. 첫 편은 무엇을 가져왔고 무엇을 새로 만들었는지, 초기 커밋과 현재 코드를 나란히 펼쳐 보는 기록이다.
출처 대조 · 원고 갱신 2026.10.16
원고 읽기 ↓

GitHub에서 가져온 기반
출발점은 빈 캔버스 위에 편집기를 전부 발명하는 일이 아니었다. React는 화면과 상태를 구성하고, 초기 PixiJS는 이미지 레이어를 화면에 그리는 역할을 맡았다. 아래 코드는 포토샷의 초기 공개 커밋이다. pixi.js에서 Application, Sprite, Texture를 불러온다. 단순히 이름만 참고한 라이브러리가 아니라 실제 실행 경로에 들어간 의존성이었다.
여기서 “GitHub 코드를 가져왔다”는 말은 저장소 전체를 복제해 완성품으로 붙였다는 뜻이 아니다. 패키지를 설치해 공개 API를 사용했고, 그 위에 포토샷의 작업 방식을 연결했다.
'use client';
import { useEffect, useRef } from 'react';
import { Application, Sprite, Texture } from 'pixi.js';
import 'pixi.js/advanced-blend-modes';
import { layerSurface, type StudioDoc } from './document';AI가 쓴 것은 연결부였다
라이브러리는 레이어의 위치나 회전, 불투명도가 포토샷 문서의 어디에 저장되는지 모른다. AI와 작성한 연결 코드는 문서의 레이어를 읽고 Sprite를 만든 뒤, 위치·회전·불투명도·합성 모드를 PixiJS에 전달했다. 아래의 l은 포토샷 레이어 데이터이고 s는 PixiJS의 화면 객체다.
이 구간이 조합의 핵심이었다. 렌더러를 재발명하는 대신 검증된 도구를 쓰되, 포토샷의 데이터와 도구가 서로 이해하도록 코드를 새로 작성했다.
const s = new Sprite(Texture.from(canvas));
s.anchor.set(0.5);
s.position.set(l.x + l.width / 2, l.y + l.height / 2);
s.rotation = (l.rotation * Math.PI) / 180;
s.alpha = l.opacity;
s.blendMode = l.blend;
a.stage.addChild(s);마스크는 원본을 지우지 않는 코드로
레이어가 보인다고 편집기가 완성되지는 않았다. 마스크를 칠해도 원본 픽셀은 남아 있어야 하고, 마스크를 끄면 원래 모습이 돌아와야 했다. Photopea의 마스크 설명은 동작을 확인하는 참고 자료였고, 포토샷의 마스크 연결 코드는 별도로 작성했다.
현재 공개 코드에서는 임시 레이어 표면에 destination-in 합성을 적용한다. 이미지와 마스크가 겹치는 불투명도만 남긴 뒤 source-over로 되돌린다. 원본 이미지 데이터와 마스크를 따로 보관하는 구조가 이 동작을 가능하게 한다.
if (layer.mask?.enabled) {
ctx.globalCompositeOperation = 'destination-in';
ctx.drawImage(maskSurface(layer.mask), 0, 0, c.width, c.height);
ctx.globalCompositeOperation = 'source-over';
}초기 PixiJS와 현재 Canvas를 구분하기
개발이 진행되면서 표시와 내보내기를 같은 합성 경로로 맞추는 일이 중요해졌다. v0.16.0의 Stage는 초기 Sprite 생성 코드 대신 composite를 호출한다. 미리보기 크기를 제한하고, 사용자가 더 새롭게 편집하면 오래된 계산을 취소하는 조건도 넘긴다.
따라서 현재 화면을 두고 “PixiJS가 전부 합성한다”고 설명하면 개발 과정이 뒤섞인다. 초기에는 PixiJS를 실제로 사용했고, 후속 버전에서는 Canvas 기반 공통 합성 코드로 경로가 바뀌었다. GitHub의 시점별 코드를 확인해야 드러나는 차이다.
const image = await composite(d, false, {
maxEdge: 1600,
cancelled: () => !mounted.current || r !== revision.current,
});
if (!mounted.current) break;화면보다 먼저 확인한 것
아래 자료는 포토샷의 실제 편집 화면과 구현 기록이다. 라이브러리가 연결됐는지만 보는 대신 레이어 순서, 마스크 켜기·끄기, 저장 후 다시 열기, 내보내기까지 이어지는지 확인했다. 마스크 렌더 테스트에는 원본을 바꾸지 않는지 점검하는 항목도 남겼다.
AI가 코드를 빨리 써 주는 것과 편집 결과가 보존되는 것은 별개의 문제였다. 가져온 라이브러리의 책임과 우리가 작성한 문서·저장·합성 코드의 책임을 나눠 확인해야 문제가 난 위치를 찾을 수 있었다.


이번 편에서 가져온 것과 만든 것
가져다 쓴 것은 React와 초기 PixiJS의 기능이었다. 참고한 것은 마스크의 동작 설명이었다. AI와 새로 작성·수정한 것은 레이어 데이터를 화면 객체에 연결하는 코드, 마스크 적용, 저장 구조, 이후의 공통 합성 경로였다. GitHub에서 찾은 기반 위에 이 접착 부분을 만드는 일이 무료 포토샵 개발의 시작이었다.
다음 편에서는 Konva의 대비 필터를 실제로 읽고, 포토샷의 밝기·대비 계산을 구현한 뒤 결과가 어긋나 다시 고친 과정을 코드로 살펴본다. 이 글의 코드는 초기 공개 커밋과 v0.16.0에서 발췌했다.