RN에서 이미지를 픽셀 이미지로 렌더하는 npm 라이브러리 개발기

희썽·2025년 1월 31일
21
post-thumbnail

프로젝트를 진행하던 중, 과일 찾기 게임을 개발하는 파트를 맡게 되었습니다. 게임에 현실감을 주기 위해 배경을 도트 이미지로 표시하면 좋겠다고 생각했고, 구글에서 이미지를 찾아 도트 이미지로 변환하여 처음에는 이를 사용했습니다. 또한, 게임에서 3개의 이미지가 랜덤으로 렌더링되도록 설정하였습니다.

이 방식이 초기에는 쉬워 보였지만, 리팩토링 과정에서 3개가 아닌 10개 이상의 이미지를 목표로 삼으면서 비효율적이라는 점을 깨달았습니다. 이에 따라, npm 사이트에서 여러 이미지를 도트로 변경해 주는 React Native 라이브러리를 찾아보았지만, 대부분 4~5년 전에 개발된 것으로 최신 버전과 호환되지 않았습니다. 또한, 네이티브 환경에서 이미지를 픽셀 이미지로 변환하는 적절한 라이브러리가 없다는 것도 확인하였습니다. 그래서 직접 개발하기로 하였습니다.

React-Native-boundary-imagepixel

저희 팀은 React Native를 사용하면서 네이티브 환경에서 실행하는 것보다 Expo Web에서 실행하는 경우가 더 많았습니다. 이에 따라, 네이티브 환경과 Expo Web 환경 모두를 고려하여 개발을 진행하였습니다.
Expo Web은 엄연한 웹 환경이었기 때문에 canvas 요소를 활용하여 픽셀 이미지를 생성할 수 있었지만, 네이티브 환경에서는 canvas를 사용할 수 없었습니다. 따라서, 네이티브 환경에서는 react-native-skia 라이브러리를 사용하여 개발을 진행하였습니다.

라이브러리를 만들 때 제가 생각한 알고리즘은 다음과 같습니다.

  1. 이미지를 화면에 로드하고, 캔버스에 그린 후 블록 단위로 변환한다.
    • 웹 환경에서는 canvas 요소를 활용하여 이미지를 픽셀화 한다.
    • 네이티브 환경에서는 react-native-skia를 활용하여 Skia 캔버스에 이미지를 그린다.
  2. 사용자는 blocksize 값을 조절해 (ex. blockSize={20}) 이미지에서 도트 크기를 조절 할 수 있다.

위 알고리즘과 같이 가장 고민해야하는 점은 웹 환경 (expo web)과 네이티브 환경 둘 다 사용할 수 있도록 개발해야한다는 점이였습니다.

저는 구글 플레이스토어에만 앱을 출시한다는 생각으로 앱을 개발해 os에 따라 다르게 동작하는걸 구현해본 적이 없었습니다. os에 따라서 다르게 동작하기를 구글에 검색했고, OS에 따라서 다르게 적용 시킬 수 있는 Platform 모듈이라는 것을 발견하게 되었습니다.

expo web에 적용하기

아래 코드에서 Platform.OS === 'web' 조건을 사용하여, OS가 웹인 경우 해당 코드가 실행되도록 구현하였습니다.

useEffect(() => {
  if (Platform.OS === "web") {
    const loadImage = async () => {
      const img = new Image();
      img.crossOrigin = "Anonymous";
      img.src = imageUri;
      img.onload = () => {
        setImage(img);
        setCanvasSize({ width: img.width, height: img.height });
        
        const canvas = canvasRef.current;
        if (!canvas) return;
        const context = canvas.getContext("2d");
        canvas.width = img.width;
        canvas.height = img.height;
        
        context.drawImage(img, 0, 0);

        const adjustedBlockSize = Math.floor(img.width / Math.ceil(img.width / blockSize));
        
        for (let y = 0; y < img.height; y += blockSize) {
          for (let x = 0; x < img.width; x += adjustedBlockSize) {
            const pixel = context.getImageData(x, y, 1, 1).data;
            context.fillStyle = `rgba(${pixel[0]}, ${pixel[1]}, ${pixel[2]}, ${pixel[3] / 255})`;
            context.fillRect(x, y, adjustedBlockSize, blockSize);
          }
        }
      };
    };

    loadImage();
  }
}, [imageUri, blockSize]);

코드를 간단하게 설명하면 이미지를 캔버스에 그리고 그려진 이미지에서 getImageData(x, y, 1, 1).data를 사용해 픽셀이 들어갈 위치의 색상을 추출합니다. 추출한 색상을 fillStyle을 통해서 픽셀 색상으로 설정하고, fillRect(x, y, adjustedBlockSize, blockSize)를 사용해 픽셀 크기만큼 fillStyle에서 가져온 색상을 채웁니다. 마지막으로 adjustedBlockSize를 계산하여 사용자가 지정한 크기 (ex. 10)만큼 픽셀을 채웁니다.

위 방식으로 그려진 픽셀 이미지를 canvas를 사용해 아래와 같이 화면에 렌더링합니다.

if (Platform.OS === "web") {
  return (
    <View style={styles.container}>
      <canvas ref={canvasRef} style={{ ...styles.canvas, width: canvasSize.width, height: canvasSize.height }} />
    </View>
  );
}

네이티브 환경 (Android, ios)에 적용하기

네이티브 환경에서는 Platform.OS === native라는게 없었고, 제가 찾아본 블로그에서는 Android, ios 각각 적용시키는 방법이 있었습니다.

xtring.dev님의 블로그

저는 네이티브 환경 Android, ios 전체를 명시하고 싶었기에 !== 문법을 사용해 보았고, 다행히도 적용이 되었습니다. 네이티브 환경에서는 웹에서 사용하는 canvas를 사용할 수 없기에 @shopify/react-native-skia 라이브러리를 사용해서 캔바스를 렌더링 하였습니다.

대부분의 알고리즘은 web에서와 네이티브 환경에서 비슷했고, 네이티브에서 사용하는 skia 라이브러리에서 캔버스에 픽셀 (사각형)을 그리는 과정이 달랐습니다.

  if (Platform.OS !== "web" && skiaCanvasRef.current && image) {
    const canvas = skiaCanvasRef.current.getContext();
    const { width, height } = image;
    const pixels = image.readPixels();

    const adjustedBlockSize = Math.floor(width / Math.ceil(width / blockSize));

    if (pixels) {
      canvas.clear();  
      for (let y = 0; y < height; y += blockSize) {
        for (let x = 0; x < width; x += adjustedBlockSize) {
          const pixelIndex = (y * width + x) * 4;
          const r = pixels[pixelIndex];
          const g = pixels[pixelIndex + 1];
          const b = pixels[pixelIndex + 2];
          const a = pixels[pixelIndex + 3] / 255;

          const paint = new Paint();
          paint.setColor(Skia.Color(r, g, b, a));
          canvas.drawRect(
            Skia.Rect.MakeXYWH(x, y, adjustedBlockSize, blockSize),
            paint
          );
        }
      }
    }
  }
}, [skiaCanvasRef, image, blockSize]);

위 코드는 image.readPixels()을 사용해 RGBA 픽셀 데이터를 가져온 뒤, 사용자 설정 크기(blockSize)로 캔버스를 나눕니다. 그런 다음, 각 위치의 색상을 추출하여 Skia.Color(r, g, b, a)로 설정합니다. 가져온 색상 값을 바탕으로 canvas.drawRect()를 사용해 사각형을 그리고, canvas.clear()를 호출하여 이전 캔버스 (픽셀로 바꾸기 전 기본 이미지)를 정리한 후 새로운 이미지로 업데이트 하게 됩니다. 이 방식으로 네이티브 환경에서 이미지를 픽셀 형식의 이미지로 라이브러리가 바꿔줍니다.

추가 개발

개발을 완료하고, 침대에 발뻣고 누워 깃허브 탐색을 살피던 도중 누군가 이슈를 작성해 놓은 것을 확인하게 되었습니다. 알고보니 좀전에도 저와 이야기를 나눈 친한 학교 선배였고, 제가 라이브러리를 세계 많은 사람이 사용하길 원하는 마음에 영어로 적어놨더니 이슈도 영어로 받게 되었습니다. issue #1

이슈 내용

😇 제안
Hi team! Thanks for made creativity library.
I give you one of suggestions; convert this to pure javascript library.
✅ 어떻게 하면 될까요? (tasks)
if @Shopify have react native feature and react too, this library can be convert to pure javascript.
I want opt-in feature that developer can choose react or react-native.

이것을 자바스크립트 라이브러리도 변환해 현재는 리엑트 네이티브만 라이브러리를 사용할 수 있지만 리엑트에서도 사용할 수 있도록 개발을 하고, 사용자가 선택할 수 있도록 리엑트/리엑트 네이티브 옵트인 기능을 원한다. @Shopify는 리엑트 네이티브 뿐만 아니라 리엑트에서도 사용할 수 있으니 가능할 것 같다라는 이슈가 도착하였습니다.

이 이슈에 저는 개발할 수 있을 것 같다고 생각하고, 현재 개발 중에 있습니다.

하지만 추가적인 개발을 하면서 src/index.js에 리엑트 적용과 관련된 코드를 추가해 구현 할 수 있겠다고 생각했지만 제가 짠 코드에서는 리엑트 네이티브 모듈인 View와 Platform을 다르게 우회할 방법을 찾지 못해 다른 구현 방법을 생각 중입니다. 구현에 성공하면 라이브러리 이름을 boundary-imagepixel로 바꿀 예정이며 이 블로그에 이어서 글을 쓸 예정입니다.

기여

https://github.com/Boundary-org/react-native-boundary-imagepixel 설명한 라이브러리에 대한 레포지토리이며 누구든 기여를 환영합니다.

profile
행복추구

0개의 댓글