브라우저에서 이미지를 자를때 고려할것들
들어가며
현재 개발중인 서비스에서, 이미지와 관련된 프론트 개발을 여럿 진행했습니다.
그중 개발 과정에서 고생도 많이 하고, 배운 점도 많았던 기능이 바로 이미지 크롭 기능이었는데요.
이번 포스트에서는 이미지 크롭 기능을 서비스에 도입하면서 배운 점들과 고민했던 것들, 고려해야 했던 것들, 마지막으로 개선할 점들을 적어보고자 합니다.
TL:DR
- 라이브러리는 요구사항을 충족하면서 가능한 용량이 작은 것을 선택하자.
- 이전 페이지와 독립적이라면 페이지, 아니라면 모달로 구현하자. 이미지 크롭의 경우에는 모달이 적절하다.
- 이미지와 캔버스는 픽셀 단위의 래스터 이미지로, width 나 height 값으로 소수점을 갖지 않음에 주의하자.
- 이미지 크롭의 역할은 로컬 이미지 선택과 이미지 적용의 중간에 있다. 사이드 이펙트 뿐만 아니라 이미지 크롭이 제거될 상황까지도 고려하자.
대상 독자
이 글은 다음과 같은 사전지식이 있는 독자를 대상으로 합니다.
- HTML, CSS, JavaScript 에 대한 기본적인 이해
- SPA(Single Page Application) 에 대한 이해
- 래스터이미지에 대한 이해
이 글에서는 다음과 같은 내용을 다룹니다.
- 이미지 크롭에 대한 개념
- 이미지 크롭을 구현할 때 고려해야 할 것들
이 글에서 다음과 같은 내용은 다루지 않습니다.
- 이미지 크롭 기능을 구현하는 방법
- 이미지 크롭 라이브러리 사용방법
고려할 것 0. 이미지 크롭, 무엇이며 왜 필요할까?
페이지에 40x40 사이즈의 유저 프로필 이미지를 표시하는 시나리오를 상상해 봅시다.
개발자 입장에서는 유저가 어떤 크기와 비율의 이미지를 설정할 지 미리 알 수 없습니다.
항상 유저가 주어진 비율의 이미지만 사용해준다면 좋겠지만, 보통 유저는 비율까지 신경써가면서 이미지를 보관해두지는 않습니다.
40x40 사이즈와 다른 크기, 혹은 비율의 이미지를 가진 유저가 프로필 이미지를 설정하기 위해서는, 이미지가 해당 영역에 맞게 줄어드는 과정에서 일부 잘리는 것을 감수하거나, 포토샵 등에서 이미지를 자른 후 다시 돌아와야 한다는 불편함이 있습니다.
해당 과정에서 귀찮음을 느낀 유저가 다시 애플리케이션으로 돌아와줄지는 알 수 없는 일입니다.
그렇기 때문에, 많은 서비스들은 유저 편의성을 높이기 위해 이미지 업로드 이전에 이미지를 자르는 기능을 제공하곤 합니다.
이 때, 이미지를 자르는 기능을 크롭(crop) 이라고 합니다.

프로필에 들어갈 이미지를 선택하고, 크롭하고, 업로드하는 기능이 들어간 웹 애플리케이션
고려할 것 1. 요구사항 정리
프로필 이미지에 크롭 기능을 넣는 것을 가정했을 때, 구현 이전에 그려본 이미지 크롭의 대략적인 사용 시나리오는 아래와 같습니다.
프로필 이미지 업로더를 클릭합니다.
파인더 (윈도우 사용자의 경우 파일 탐색기)가 열리고, 사용자가 이미지를 선택합니다.
이미지 크롭 창이 열리고, 사용자가 선택한 이미지가 크롭 가이드 영역과 함께 창 내에 그려집니다.
사용자가 크롭 가이드 영역을 늘리거나 줄이고, 이동함으로써 원하는 이미지 영역을 선택한 뒤, 완료 버튼을 누릅니다.
크롭 가이드에 의해 선택된 영역이 프로필 이미지로 설정됩니다.
이전의 애플리케이션에서 이미 제공하고 있던 기능은 위 프로세스에서 1-> 2-> 5 에 해당했습니다.
사용자가 로컬에서 이미지를 선택하면, 해당 이미지가 바로 사용자의 프로필 이미지로 설정되었던 것이죠.
이제 기존의 프로필 이미지 설정 프로세스에 3, 4 번을 끼워넣는 것이 제가 풀어야 할 문제였습니다.
고려할 것 2. 라이브러리 선택
이미지 크롭 기능 개발을 손쉽게 할 수 있도록 도와주는 많은 라이브러리가 있는데, 저는 react-easy-crop 과 react-image-crop 중에서 고민을 했습니다. 두 라이브러리의 차이를 간단히 정리해 보면 다음 표와 같습니다.
| react-easy-crop | react-image-crop | |
|---|---|---|
| 용량 | 494kb | 110kb |
| 사용경험 | 무 | 유 |
| 추가 기능 | 이미지 줌 인/아웃, 비디오 크롭, etc | 없음 |
| Github Star | 2.1k | 3.6k |
| last updated at | 1 month | 1 week |
상대적으로 더 우위에 있다고 생각한 항목을 볼드처리했습니다.
- 용량 : 라이브러리 용량이 작아야 전체 애플리케이션의 용량도 가벼워집니다. react-image-crop 의 압승입니다.
- 사용경험 : 해당 라이브러리를 다시 선택할 만큼, 이전에 사용해본 경험이 나쁘지 않았습니다.
- 추가 기능 : 라이브러리에서 추가적인 기능을 더 제공하는 건 분명 이점입니다. 나중에 요구사항이 추가되어 해당 추가기능이 필요할 수도 있기 때문입니다. 하지만 지금 당장의 요구사항은 단순히 인풋 이미지를 잘라서 업로드 할 수만 있으면 되기 때문에, 이미지 줌이나 비디오 크롭 등의 언제 추가될지도 모르는 요구사항을 위해 더 크고 복잡한 번들을 선택하고 싶지는 않았습니다.
- Github Star :
star 가 많을수록 좋은 라이브러리이다라는 인과가 항상 성립하는 것은 아니지만, 해당 라이브러리의 신뢰도를 나타내는 중요한 지표임에는 틀림없습니다. - last updated at : 해당 라이브러리가 아직 maintainer 에 의해 유지보수되고 있는지가 중요했습니다. 왜냐하면 라이브러리를 사용하다가 문제가 생기거나, 기여할 만한 요소를 찾았을 때 maintainer 가 해당 라이브러리에 관심을 끊은 상태라면, 혼자서 문제를 해결해나가야 할 수도 있기 때문입니다. 다행히도 두 라이브러리 모두 maintainer 가 최근 라이브러리를 업데이트했다는 사실을 확인했고, issue 탭과 댓글창에서도 활발하게 커뮤니케이션이 오가고 있는 것을 확인했습니다.
위의 요소들을 고려한 끝에, 제공하는 기능이 요구사항을 충족하기에 부족함이 없고, 전체 애플리케이션에 부하를 최대한 덜 줄 수 있는 라이브러리인 react-image-crop을 선택하게 되었습니다.
특정 라이브러리를 선택했지만, 이 글에서는 최대한 라이브러리에 편중된 이야기는 빼고 이미지 크롭의 본질에 대한 이야기를 다룹니다.
고려할 것 3. 이미지 크롭 화면은 페이지일까? 모달일까?
SPA 의 구조를 이용하면, 페이지가 바뀌어도 상태를 유지할 수 있습니다. 따라서 페이지든 모달이든 이미지 크롭 화면을 구현하는 데에 큰 문제는 없습니다.
그러나 만약 개발자에게 선택권이 있는 상황이라면, 저는 다른 화면과 독립적으로 존재할 수 있는 경우에는 페이지, 그렇지 않은 화면이라면 모달을 선택합니다. 이러한 방법은 이미지 크롭 화면 외에도 미리보기 화면 등을 구현할 때에도 잘 들어맞고 있습니다. (회원가입 절차가 여러 페이지에 걸쳐 이뤄지는 절차 화면 같은 경우에는 페이지가 나을 수도 있습니다)
크롭할 이미지가 뜨는 화면은 이전 화면에서 사용자가 프로필 이미지 설정 버튼을 누르고, 이미지 파인더의 이미지를 선택하지 않았다면 볼 수 없는 화면입니다. 이미지 크롭 화면은 페이지 내의 이미지 업로드 버튼에 의존하고 있는 것이죠.
따라서 저는 이미지 크롭 화면을 모달로 구현했습니다.

왼쪽이 이미지 크롭, 오른쪽이 메인 페이지 미리보기입니다. 두 화면 모두 페이지처럼 보이지만, 화면을 다 덮는 형태의 모달입니다.
고려할 것 4. 크롭된 이미지는 canvas 위에 그려진다
크롭이 작동하는 방식을 요약하면, 크롭 가이드 영역의 크기와 위치 값을 토대로, canvas 의 2D context 객체가 가지고 있는 drawImage() 메서드를 통해 원본 이미지를 canvas 위에 그리는 것입니다. 따라서 이미지 크롭 기능을 개발할 때에는 canvas 를 빼놓고는 이야기할 수 없습니다. 이번 장에서는 canvas 를 이용하는 과정에서 고려해야 할 점들을 다룹니다.
캔버스의 사이즈는 점선으로 표현된 크롭 가이드 영역의 크기와 원본 이미지가 축소, 혹은 확대된 정도를 곱하여 얻을 수 있습니다.
이렇게 마련된 캔버스 위에 2DContext.drawImage() 을 실행하여 이미지의 일부 영역만을 그립니다.
4-1. 이미지의 원본 크기와 화면에 그려진 크기가 다를 수 있다
이미지가 그려지는 화면의 크기에 따라 다르겠지만, 이미지를 크롭하는 화면이 이미지의 원본 크기보다 작은 경우가 많습니다. 예를 들면, 이미지의 원본 크기는 3000x2000 인데에 비해 이미지를 그릴 화면의 크기는 1440x660 밖에 안되는 것이죠. 우리가 캔버스 위에 그릴 크롭 이미지는 원본 이미지를 잘라 그려지는 것이므로, 이미지의 원본 크기와 화면에 그려진 크기를 고려하여 canvas 의 사이즈를 구해야 합니다.
이때 크롭된 이미지를 그릴 canvas 의 width 와 height 를 구하는 식은 다음과 같습니다.
const canvas = document.createElement('canvas');
const imageWidthScale = (image.naturalWidth / image.width);
const imageHeightScale = (image.naturalHeight / image.height);
canvas.width = crop.width * imageWidthScale;
canvas.height = crop.height * imageHeightScale;
image 의 widthScale과 heightScale 을 canvas 의 크기 할당에 사용하는 이유는, 앞서 얘기했듯 크롭 가이드 영역이 캔버스에 옮겨야 할 이미지 영역의 사이즈 정보를 정확하게 알아야 하기 때문입니다. 이미지의 원본 크기만을 고려하거나, 반대로 이미지가 화면에 그려진 크기만을 고려하면 자칫 너무 작거나 큰 캔버스 사이즈를 얻을 수 있습니다.
4-2. 원본 이미지와 캔버스의 비율이 100% 같을 수는 없다
캔버스의 사이즈는 항상 정수 값만을 갖습니다. 만약 정수로 떨어지지 않는 값이 들어오면 내림으로 처리합니다. 이러한 내림 처리로 인해 캔버스가 원본 이미지의 비율을 유지하지 못하는 경우가 있습니다. 예시를 통해 보겠습니다.
위의 식을 잠시 다시 언급하면, 캔버스의 사이즈에 들어가는 값 중에는 나눗셈으로 인해 생성되는 값인 imageWidthScale 과 imageHeightScale 이 있습니다. 이 두 값은 나눗셈의 결과값이 정수일 때에는 아무런 문제가 되지 않습니다.
예를들어 이미지 원본의 크기가 3000x2000 이고, 좁은 화면 폭에 의해 이미지의 렌더링 크기가 1500x1000이 된 경우를 식에 대입해보면, 캔버스의 width 와 height 가 모두 80px 로, 우리가 원하는 1:1 비율을 얻을 수 있습니다.
imageWidthScale = 3000 / 1500 // 2
imageHeightScale = 2000 / 1000 // 2
canvas.width = 40 * 2 // 80
canvas.height = 40 * 2 // 80
// canvas 의 최종 크기 : 80x80
그러나, 반응형 컨테이너에 의해 렌더링 크기가 소수점을 갖게 되는 경우가 있습니다. 이는 이미지가 원본의 비율을 유지하기 위해 줄어들면서 일어나는 현상입니다. 그럼에도 불구하고, javaScript 로 조회할 수 있는 width 또는 height 는 반올림 된 값을 반환합니다.

브라우저에 표현된 height 값과 이미지의 Rendered size 가 서로 다른 걸 알 수 있습니다. javaScript 로 이미지의 width 나 height 값을 조회하면 Rendered size 를 반환합니다.
이로인해 imageWidthScale 과 imaegHeightScale 두 값이 서로 다른 값을 갖게 되면서, 최종적으로 캔버스에 할당되는 width 와 height 역시 서로 다른 값을 갖게 됩니다.
예를들어 이미지 원본의 크기가 1125x2184 이고, 좁은 화면 폭에 의해 이미지의 렌더링 크기가 311x604 가 된 경우를 위의 식에 대입해보면, 캔버스의 width 와 height 에 서로 다른 값이 할당되는 것을 알 수 있습니다.
imageWidthScale = 1125 / 311 // 3.6173633441
imageHeightScale = 2184 / 604 // 3.6158940397
canvas.width = 40 * 3.6173633441 // 144.694533764
canvas.height = 40 * 3.6158940397 // 144.635761588
// canvas 의 최종 크기 : 144x144
앞서 얘기했듯, 캔버스의 사이즈는 항상 정수 값만을 갖습니다. 만약 정수로 떨어지지 않는 값이 들어오면 내림으로 처리합니다. 따라서 위처럼 캔버스의 width 와 height 가 소수점이 다른 값을 갖더라도 크게 문제가 되지는 않습니다. 캔버스는 두 수를 모두 내림 처리하여 결국에는 우리가 원하는 비율인 1:1의 144x144 캔버스 사이즈를 갖게 될 테니까요.
그러나, 서로 다른 두 수를 각각 내림 처리 한다는 것이 반드시 두 수를 같은 정수로 만들어주지는 않습니다. 이번에는 이미지 원본의 크기가 3024x4032 이고, 좁은 화면 폭에 의해 이미지의 렌더링 크기가 298x397이 된 경우를 식에 대입해 보겠습니다. (크롭 가이드 영역의 크기는 120x120 입니다)
imageWidthScale = 3024 / 298 // 10.1476510067
imageHeightScale = 4032 / 397 // 10.1561712846
canvas.width = 120 * 10.1476510067 // 1217.718120804
canvas.height = 120 * 10.1561712846 // 1218.740554152
// canvas 의 최종 크기 : 1217 x 1218
내림으로 인해 canvas 의 최종 width 와 height 가 서로 다른 값을 갖게 되었습니다. 이처럼 canvas 는 자신에게 할당된 width 와 height 값을 내림 처리하므로, 크롭된 이미지의 비율이 원본 이미지와는 다른 비율을 갖게 될 수 있다는 것을 고려해야 합니다.
만약 이를 고려하지 않고, 1:1 비율의 이미지를 요구하는 컨테이너에 다른 비율의 이미지를 넣으면 이미지가 자칫 깨진 것처럼 보일 수 있습니다. 이는 object-fit: cover 속성을 적용해도 해결할 수 없는 문제로, 이와 같은 현상에 대해서는 다른 포스팅에서 좀 더 다뤄볼 예정입니다.
고려할 것 5. 크롭의 역할은 어디까지일까?
위의 요구사항 정리에서 살펴봤던, 이미지 크롭 기능을 사용하는 사용자 여정을 다시 한 번 나열해보면 다음과 같습니다.
프로필 이미지 업로더를 클릭합니다.
파인더 (윈도우 사용자의 경우 파일 탐색기)가 열리고, 사용자가 이미지를 선택합니다.
이미지 크롭 창이 열리고, 사용자가 선택한 이미지가 크롭 가이드 영역과 함께 창 내에 그려집니다.
사용자가 크롭 가이드 영역을 늘리거나 줄이고, 이동함으로써 원하는 이미지 영역을 선택한 뒤, 완료 버튼을 누릅니다.
크롭 가이드에 의해 선택된 영역이 프로필 이미지로 설정됩니다.
여기서 이미지 크롭의 역할은 어디까지일까요?
저는 3번과 4번 까지가 이미지 크롭의 역할이라고 생각했습니다. 위의 1~5번에 해당하는 모든 기능을 이미지 크롭의 역할로 보지 않았던 이유는, 이미지를 선택하는 도메인이 어디냐에 따라 크롭을 사용하지 않는 곳도 있었기 때문입니다. 예를 들어, 프로필이 아닌 게시글 이미지 업로드를 하는 경우에는 2번에서 여러 이미지를 한 번에 선택할 수도 있고, 그렇게 선택한 이미지가 바로 게시글 에디터에 반영되어야 했습니다.
따라서 이미지 크롭을 구현할 때는 원래 이미지 크롭이 없었을 때의 기존 모듈들의 인터페이스를 최대한 유지하려 노력했습니다. 모종의 이유로 이미지 크롭 기능을 다시 들어내야 한다고 했을 때, 기존 이미지 업로드 프로세스가 동작했던 방식인 1 -> 2- > 5 로 쉽게 돌아갈 수 있게끔 말이죠. 이처럼 프로세스의 중간에 기능을 끼워넣을 때에는 해당 기능이 불러올 사이드 이펙트 뿐만 아니라, 해당 기능을 들어내야 할 상황까지 고려해야 합니다.

File 타입의 image 인터페이스를 그대로 유지함으로써, 이미지 크롭 모듈이 기존 프로세스에 자연스럽게 녹아들도록 했습니다.
마치며
개인적으로 시각적인 작업물을 좋아하기도 하고, 대부분의 이미지 업로드 기능에 크롭 기능이 들어갔기 때문에 많은 유저에게 노출될 수 있다는 점에서 재미있는 작업이었습니다.
아쉬웠던 점은, 빠르게 문제를 해결하려다가 오히려 문제 해결이 더 늦어지는 등, 이슈가 되었던 문제들에 대해 근본적인 원인을 깊게 파헤치지 않았던 점이 아쉬웠습니다. 앞으로는 문제 해결에 필요한 지식 습득에 소모되는 시간을 너무 아까워하지는 말아야겠다는 생각이 듭니다.
마지막으로, 시작은 단순 크롭이었지만 이미지와 직접적인 연관이 있다보니 이미지와 관련된 다양한 지식들을 학습할 수 있어서 좋았습니다...만 아직도 모르는 것이 너무 많아서 기대도 되고 걱정도 되는 부분입니다.