냄궁밈수 (밈 생성기)

🧤 프로젝트 개요
원하는 사진을 고른 뒤, 센스 있는 문구를 넣어 자신만의 재밌는 밈을 생성할 수 있는 웹 페이지
📲 링크
Git. https://github.com/hoonjoo-park/Namgoong-Meme-Soo
배포 주소. https://namgoong-meme-soo.vercel.app/
🤷 사이트 이름이 왜 냄궁밈수인가..?

사실 특별한 이유는 없다.. 그냥 친구들과 얘기를 나누던 중 ‘밈‘이 들어가는 사이트 이름 작명을 부탁해봤다.
그러다가 그냥 우스갯 소리로, 영화 설국열차에서 크리스 에반스의 인상 깊은 대사였던(?) “Are You 냄궁밈수?”라는 헛소리가 나왔다. 그래서 친구들과 웃다가 “그래~ 그냥 그걸로 하자~”하며 사이트 이름이 냄궁밈수가 되었다.
🚚 밈 텍스트 박스 드래그 이동 기능

입력한 밈 텍스트가 마우스 드래깅을 통해 원하는 위치로 이동될 수 있도록 기능을 구현했다.
밈 제작의 기본 중 기본은, 텍스트 엘리먼트를 마우스 드래깅을 통해 위치를 자유자재로 조정할 수 있도록 하는 것이라 생각했다. 이에 따라 여러 고민을 하던 중, 예전에 드래그&슬라이드 기능을 구현했던 방법과 유사한 접근 방법을 사용하여 엘리먼트 이동 기능을 구현해보기로 결정했다.
onMouseDown(시작) →onMouseMove(이동) →onMouseUp(종료)
이와 같은 로직을 따르면 될 것 같았기에 위의 세 이벤트 리스너와 핸들링 함수를 사용했다.
onMouseDown
우선, 텍스트 박스 위에서 mouseDown이 됐을 때 저장해야 하는 값들이 꽤 많았다.
- 마우스의 초기 좌표값 :
e.pageX,e.pageY - 텍스트 박스의 초기 위치값 :
offsetLeft,offsetTop - 마우스가 다운됐는지 여부 : isDown(
boolean)
이러한 값들을
handleMouseDown이라는 이벤트 핸들러 함수에서 이하와 같이 처리해줬다.
1 | const handleMouseDown = (e: React.MouseEvent<HTMLElement>) => { |
onMouseMove
mousemove 이벤트의 경우에는, 마우스가 텍스트 박스 위에서 움직일 때를 리스닝 하는 것이 아닌, window 위에서 마우스가 움직이는 것을 리스닝 하도록 처리해줘야 했다. 박스 위에서의 mousemove를 리스닝하도록 하면, 마우스가 박스 밖으로 나갔을 때 텍스트박스가 이동을 멈추는 문제들이 발생했기 때문이다.
따라서, 위에서 사용했던
isDown을 활용해window에addEventListener를 통해mousemove이벤트 리스너를 추가해줬다.
1 | // isResizing은 아래에서 설명할 텍스트박스 리사이징 기능에서 활용되는 state다. |
즉, mouseDown이 됐을 때 → useEffect를 통해 window에 mousemove 이벤트 리스너가 활성화 된다.
이제 마우스가 움직일 때를 핸들링하는 함수 handleMouseMove에 대해 다뤄보도록 하겠다.

1 | const handleMouseMove = (e: MouseEvent) => { |
좀 더 쉽게 설명하자면, 위의 사진과 같이 마우스를 클릭한 뒤,마우스가 움직인 거리만큼 텍스트박스가 이동되는 것이다.
조금 더 자세히 toMoveLeft와 연관지어 풀어 써보자면, 아래의 사진과 같이 표현할 수 있을 것이다.

이 때문에 위에서 const toMoveLeft = e.pageX - startX + startLeft 라는 계산식이 도출된 것이다.
이를 다시 풀어서 설명하면,
마우스의 현재위치(e.pageX) - 처음mouseDown됐을 때의 위치(startX) = 마우스가 이동한 거리
startLeft = 원래 offsetLeft 위치
따라서 마우스가 이동한 거리 + 원래 offsetLeft 값을 더해주면 → 마우스 드래그에 따른 박스의 위치(X)값이 계산되는 것이다.
이제 값을 구했으니 textMover라는 함수를 사용해 실제 박스를 해당 위치로 이동시켜주기만 하면 된다.
1 | // inputRef는 text박스를 의미한다 |
onMouseUp
이제 텍스트 이동이 끝났을 때 이벤트들을 모두 remove 해주기만 하면 기능 구현이 모두 끝난다.
만약, onMouseUp에 따른 이벤트 리무빙을 해주지 않는다면, 텍스트가 계속해서 마우스를 따라다닐 것이다.
따라서 아래와 같이 이벤트 리스너들을 제거해줘야 한다.
1 | const handleStopMoving = () => { |
이렇게 까지만 구현해주면, 텍스트 박스가 마우스 드래깅에 따라 원하는 위치로 이동되도록 하는 기능 구현이 모두 끝났다!
물론, 추후에 코드들을 조금 더 간결하고 가독성이 좋도록 리팩토링을 더 진행해볼 계획이다.
🕹 밈 텍스트 박스 리사이징 기능

사용자 입장에서 생각했을 때, 밈 텍스트의 위치조정 뿐만 아니라 텍스트 사이즈도 자유롭게 조절될 수 있으면 훨씬 더 편하게 밈을 만들 수 있을 것이라 생각했다.
따라서 텍스트 박스 엘리먼트의 크기가 조절될 수 있도록 함과 동시에, 폰트 사이즈 또한 박스 크기에 비례하여 줄어들고 늘어날 수 있도록 기능을 구현해보고자 했다.
resizer 버튼 배치하기
위의 gif에 나타나 있듯이, 텍스트 박스 모서리에 네 개의 리사이저를 배치했다.
1 | <Text |
mouseDown 이벤트 활용하기
그리고 각 리사이저들에
onMouseDown이벤트 핸들러를 할당해줬다.
onMouseDown에 할당된 resizeStart 함수는 위에서 사용했던 handleMouseDown 핸들러 함수와 굉장히 유사하다.
1 | const resizeStart = (e: React.MouseEvent, type: string) => { |
그리고 위와 동일하게, useEffect를 활용하여 리사이징이 시작됐을 때(onMouseDown) window에 전역적으로 mousemove와 mouseup 이벤트 핸들러를 할당해 줬다.
1 | useEffect(() => { |
mouseMove 이벤트를 통해 박스 크기 조절하기
가장 어려웠던 부분이다.
처음에는 그냥 마우스의 이동 거리를 매 이벤트마다 재할당 해주며 사이즈를 줄이거나 늘려주면 될 것이라 생각했지만, mousemove 이벤트를 setState가 따라가지 못해서 정확한 사이징이 이루어지지 않았다.
따라서, 차라리 계속 변경되는 엘리먼트의 사이즈를 계속 업데이트 해주는 것 보다는, 초기 width값을 고정시켜두고 마우스의 매 이동거리가 아닌, 총 이동거리를 계속 차감해주는 방식을 선택했다.
코드는 이하와 같다.
1 | const mouseResizing = (e: MouseEvent) => { |

이 경우는 그냥 음수값만큼 startWidth에 더해주면 저절로 사이즈가 줄어들도록 값을 줄 수 있다.

하지만 이 경우는 movedX가 양수임에도, 의도한 기능은 width가 줄어들어야 하기 때문에 startWidth에서 해당 값을 차감해줘야 한다.
1 | const handleResize = (movedX: number, movedY: number) => { |
onMouseUp
위에서 사용한 mouseUp 핸들링 함수와 거의 유사하다.
1 | const stopResizing = () => { |
💾 DOM을 이미지로 저장하기
dom-to-image와 file-saver 라이브러리를 활용했다.
설치
1 | $ npm install dom-to-image file-saver |
코드 활용 로직
useRef로 이미지 박스 DOM에 직접적으로 접근한다 → dom-to-image를 통해 해당 DOM을 스크린샷 찍은 후 파일 url을 얻는다 → 해당 url을 file-saver에 전달하여 로컬에 사진이 저장될 수 있도록 한다.
1 | const saveImage = async () => { |
프로젝트 회고
음… 가벼운 마음으로 시작했지만, 굉장히 손도 많이 가고 꼼꼼함이 필요했던 프로젝트였던 것 같다.
그리고 처음으로 배포를 한 뒤에, 주변 지인들에게 사이트 활용을 부탁했고, 지인들로부터 보완사항 및 피드백을 받아 git issue에 등록했다. 그리고 매일 한 두개씩의 이슈들을 해결하려 노력했다.
특히 아래의 세 가지 기능들이 지인들로 부터 받은 피드백이었고, 해당 기능들을 추가 및 보완하기 위해 굉장히 애를 썼다.
- 텍스트 추가 및 삭제 기능
- 텍스트별 폰트 색상 적용 기능
- 텍스트박스 및 폰트 리사이징 기능
“우선 배포해라, 어차피 고쳐야 할 것들은 산더미다” 라는 말이 굉장히 와닿았던 프로젝트였다.
앞으로 다른 프로젝트를 할 때에도, 최대한 유저 입장에서 고민하며 개발을 하고, 배포 후에도 유저들의 피드백에 귀 기울일 수 있는 개발자가 되도록 노력해야겠다.
굉장히 유의미한 경험이었다. 🧏♂️