Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

grunt-spritesmith로 Retina용 Sprite 이미지 생성하기

grunt-spritesmith로 CSS Sprites 자동화하기라는 글을 올렸었는데 요즘은 모바일 대응도 중요하므로 이미지를 사용할 때 레티나 디스플레이에도 대응을 해야 한다. 일반적인 이미지는 x2, x3 같은 이미지를 준비해서 미디어 쿼리 등으로 디스플레이에 맞게 보여주지만 Sprites 같은 경우는 x2배 이미지를 Sprites로 합치면 크기도 다시 조정해야 하고 위치도 새로 조정해야 한다. 이 부분이 Sprites 이미지를 쓸 때 어려운 부분 중 하나인데 다행히고 grunt-spritesmith가 Retina 지원도 하고 있다. grunt-spritesmith의 사용법은 이전 글에 올렸으므로 여기서는 Retina 파라미터에 대해서만 정리한다.

Retina 파라미터

grunt-spritesmith로 sprite 이미지를 만드는 다음과 같은 Grunt 설정이 있다고 해보자.

grunt.initConfig({
  sprite:{
    all: {
      src: 'icons/*.png',
      dest: 'sprites/icons.png',
      destCss: 'css/sprites.css',
      cssSpritesheetName: 'icons-spritesheet'
    }
  }
});

설정에서 보듯이 Sprite 이미지로 만들 아이콘은 icons 폴더 아래 있고 Sprite 이미지는 sprites/icons.png로 생성되고 CSS는 css/sprites.css에 생성된다.

레티나 디스플레이가 나온 이후 요즘에는 모바일 기기에서 device pixel ratio가 2가 넘는 기기가 많아졌으므로 이런 기기에서도 아이콘이나 이미지가 깔끔하게 보이려면 (device pixel ratio가 2인 경우) 2배 크기의 이미지를 사용해야 한다. 이 말은 2배 크기의 아이콘으로 만든 Sprite 이미지가 추가로 필요하다는 의미인데 다행히도 grunt-spritesmith가 이를 지원한다.

icons/
├── app-2x.png
├── app.png
├── brightness-2x.png
└── brightness.png

먼저 위와 같이 icons 폴더에 2배 크기의 아이콘을 추가한다. 위에서 -2x를 파일명에 붙인 것처럼 1배 크기의 아이콘과 같은 파일명을 사용하지만 2배 크기의 아이콘을 구별할 수 있는 접미사가 붙는 형태가 좋다.

grunt.initConfig({
  sprite:{
    all: {
      src: 'icons/*.png',
      retinaSrcFilter: 'icons/*-2x.png',
      dest: 'sprites/icons.png',
      retinaDest: 'sprites/icons-2x.png',
      destCss: 'css/sprites.css',
      cssSpritesheetName: 'icons-spritesheet'
    }
  }
});

이제 Gruntfile.js를 위와 같이 수정한다. retinaSrcFiltersrc 이미지 중에서 레티나 용 아이콘을 필터링하는 역할을 한다. 그래서 전체 src에 포함된 이미지 중에서 retinaSrcFilter에 속한 이미지는 레티나용으로 처리하고 그렇지 않은 이미지는 일반 이미지로 처리해서 스프라이트 이미지를 만든다. retinaDest는 레티나용 sprite 이미지를 만들 위치를 지정한다.

$ grunt sprite
Running "sprite:all" (sprite) task
Files "css/sprites.css", "sprites/icons.png", "sprites/icons-2x.png" created.

grunt sprite를 실행하면 CSS 파일과 2개의 sprite 이미지가 생긴 것을 볼 수 있다.

.icon-app {
  background-image: url(../sprites/icons.png);
  background-position: 0px 0px;
  width: 24px;
  height: 24px;
}
.icon-brightness {
  background-image: url(../sprites/icons.png);
  background-position: -24px 0px;
  width: 24px;
  height: 24px;
}

@media (-webkit-min-device-pixel-ratio: 2),
       (min-resolution: 192dpi) {
  .icon-app {
    background-image: url(../sprites/icons-2x.png);
    background-size: 48px 24px;
  }
  .icon-brightness {
    background-image: url(../sprites/icons-2x.png);
    background-size: 48px 24px;
  }
}

여기서 생성된 CSS 파일의 내용을 보면 미디어 쿼리를 이용해서 레티나 디스플레이에서는 2배 크기의 아이콘을 사용하도록 지정한 것을 볼 수 있다. background-size가 지정된 이유는 sprite 이미지 자체가 2배 크기이므로 이를 절반 크기고 줄여서 정상적으로 표시되도록 한 것이다. 사용할 때는 .icon-app.icon-brightness같은 클래스를 사용하면 별도의 처리 없이도 레티나용 이미지를 사용할 수 있다.

주의할 점은 사용하는 아이콘이 모두 레티나용으로 있어야만 가능하고 일부만 레티나로 만드는 것은 불가능하다. 1배 크기의 이미지와 2배 크기의 이미지의 개수가 맞지 않으면 다음과 같은 오류가 발생한다.

$ grunt sprite
Fatal error: Retina settings detected but 1 retina images were found. We have 2 normal images and expect these numbers to line up. Please double check `retinaSrcFilter`.

또한, 레티나 이미지는 반드시 1배 크기의 이미지의 정확히 2배 크기여야 한다. 이 둘의 크기가 다르다면 생성 시 다음과 같이 경고가 발생한다. 이는 정확히 2배 크기가 아니면 2배 크기의 sprite 이미지의 크기도 달라지므로 background-size로 크기를 조정해서 위치를 정확히 잡을 수 없기 때문이라고 생각한다.

$ grunt sprite
Running "sprite:all" (sprite) task
>> Normal sprite has inconsistent size with retina sprite. "brightness" is 24x24 while "brightness-2x" is 46x46.
Files "css/sprites.css", "sprites/icons.png", "sprites/icons-2x.png" created.


Stylus를 사용하는 경우

SassLess 등의 전처리자를 사용하는 경우에도 레티나용 Sprite 이미지를 만들 수 있는데 여기서는 Stylus를 기준으로 설명한다. Gruntfile.js에서 destCss: 'css/sprites.css' 대신 destCss: 'css/sprites.styl'로 지정하면 된다.

$app_name = 'app';
$app_x = 0px;
$app_y = 0px;
$app_offset_x = 0px;
$app_offset_y = 0px;
$app_width = 24px;
$app_height = 24px;
$app_total_width = 24px;
$app_total_height = 24px;
$app_image = '../sprites/icons.png';
$app = 0px 0px 0px 0px 24px 24px 24px 24px '../sprites/icons.png' 'app';
$app_2x_name = 'app-2x';
$app_2x_x = 0px;
$app_2x_y = 0px;
$app_2x_offset_x = 0px;
$app_2x_offset_y = 0px;
$app_2x_width = 48px;
$app_2x_height = 48px;
$app_2x_total_width = 48px;
$app_2x_total_height = 48px;
$app_2x_image = '../sprites/icons-2x.png';
$app_2x = 0px 0px 0px 0px 48px 48px 48px 48px '../sprites/icons-2x.png' 'app-2x';

$app_group_name = 'app';
$app_group = 'app' $app $app_2x;
$retina_groups = $app_group;

2배 크기의 아이콘을 위한 각 값이 별도로 생성되므로 각각 가져다가 사용할 수 있고 sprites 믹스인 외에도 레티나 용 클래스도 같이 만들 수 있는 retinaSpriteretinaSprites도 생성된다. 사용하는 아이콘 전부를 만들려면 retinaSprites 믹스인에 $retina_groups를 전달하면 되고 개별로 따로 생성해서 사용하려면 retinaSprite 믹스인에 각 아이콘의 _group 변수를 전달하면 된다.

retinaSprite($retina_group) {
  $normal_sprite = $retina_group[1];
  $retina_sprite = $retina_group[2];
  sprite($normal_sprite)

  @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
    spriteImage($retina_sprite)
    spriteBackgroundSize($normal_sprite)
  }
}

retinaSprites($retina_groups) {
  for $retina_group in $retina_groups {
    $sprite_name = $retina_group[0];
    .{$sprite_name} {
      retinaSprite($retina_group);
    }
  }
}

레티나 파라미터 문서를 보면 여러 가지 sprite 이미지를 만들 때 서로 충돌 나지 않도록 cssRetinaSpritesheetNamecssRetinaGroupsName로 그룹이나 sprite 시트의 이름을 지정할 수 있다.

2015/05/16 23:04 2015/05/16 23:04