이번에는 프로젝트에 NFT를 끼얹어 버렸습니다 ㅋㅋ

 

특화 프로젝트에서는 하나의 특화 아이템을 선택해야 했는데, 그게 바로 NFT였습니다.

나름 최근에 핫한 주제이기도 하고, 이전부터 한번 공부해봐야지~ 라고 생각했던 소재였기에 주저없이 NFT를 선택했습니다.

심지어.. 거기에 게임까지 더해버렸습니다!!

 

그리고!!!

이전부터 소원이었던 백엔드를 이번 기회에 꼭 한번 담당해보고 싶어서 팀원들에게 부탁을 했습니다.

그래서 팀원들의 배려와 도움으로 개발기간 중 대부분의 시간을 백엔드에 할애할 수 있었습니다.

Team Classic 팀원들에게 무한한 감사 ㅎㅎ

 

목차


    0. 팀 컬쳐

    0.0. 그라운드 룰

    이전의 팀 규칙을 이번에는 Ground Rule로 표현해 보았습니다!

    코어타임과 코드리뷰, 주간테크톡 등 이전 공통프로젝트에서 해보지 못했던 것과 관련된 내용들을 정리해 두었습니다.

     

     

    0.1. 갤러리

    이번 프로젝트에서도 저번 프로젝트에서 긍정적인 반응을 이끌어냈던 갤러리 작업을 진행했습니다.

    사실 사진 찍고 관리하는 것이 쉬운 일은 아니지만, 하다보니까 재밌어서 벌써 세 번째 갤러리 운영을 하게 됐네요 ㅎㅎ

     

     

    0.2. 바로가기

    이번에는 다른 팀원이 사용하던 양식을 사용해보았습니다!

    메인에 달력을 띄워놓는게 가장 큰 차이였는데, 접근성 측면에 있어서 좋았습니다!

    앞에 꺼내둘 요소들이 많이 없다면 앞으로도 달력을 계속 꺼내놓지 않을까.. 생각해봅니다!!

     

     

    0.3. 삽질게시판

    이번에도 삽질게시판을 운영했습니다!

    하지만 대부분 저만 사용해서 아주 쪼금 아쉬웠습니다 ㅎㅎ

    다들 무난하게 프로젝트를 진행했기 때문에 그런게 아닐까~

     

     

    0.4. 주간 Tech Talk

    이전 프로젝트에서 못해서 아쉬웠다고 했던 바로 그 주간테크톡을 이번 프로젝트에서는 진행했습니다!

    매주 한번씩 자신이 공부한 내용이나 삽질한 내용에 대해서 딥하게 팀원들에게 설명하는 시간을 가졌습니다.

    팀원들과 함께 서로가 알게되거나 새로 습득하게 된 지식을 나눌 수 있어서 정말 좋은 경험이었다고 생각합니다!

     

     

    첫 주는 온라인, 두 번째 주부터는 오프라인으로 진행했는데, 오프라인으로 진행했을 때의 호응도가 더 높았기 때문에 앞으로도 이렇게 스터디를 진행하게 된다면 오프라인으로 추진하게 될 것 같습니다 ㅎㅎ

     

     

    0.5. 리뷰투게더

    리뷰투게더 역시 저번 프로젝트에서 제대로 진행하지 못했던 코드리뷰를 보다 적극적으로 진행하기 위해서 도입했습니다!

    단순히 팀 내에서 리뷰를 진행하는 것 이외에도 다른 프로젝트 팀원들과 코드를 공유하고 리뷰를 주고받을 수 있었습니다.

    리뷰를 작성하면서 내가 알고 있는 지식이 맞는 지식인가를 다시 한 번 확인하게 되고, 더 열심히 공부해야겠다는 생각을 했습니다...

     

    다만 다들 프로젝트에 몰두하느라 이전 부스트캠프에서 진행했던 것 만큼의 참여도는 나오지 않아서 다소 아쉬웠습니다.

    같은 주제로 프로젝트를 진행하는 다른 반 팀을 섭외하는 데에 굉장한 어려움을 겪었습니다.

    총 4팀에게 컨택했지만, 딱 1팀에서 긍정적으로 답변해주셔서 정말로 다행히 리뷰투게더가 폐지되지 않고 운영될 수 있었습니다! 이렇게라도 감사인사를 드립니다 ㅎㅎ

    아무래도 수상이 걸려있다보니 다들 학습적인 요소보다는 프로젝트에 조금 더 몰두한 것이 아닌가 생각합니다.

     

     

    0.6. 개선사항

    팀 내 문화는 정말로 좋았고, 개발 자체도 잘 진행되었습니다.

    다만 백엔드 학습시간때문에 개발에 많은 시간을 할애하지 못한 점은 다소 아쉽습니다. (백엔드 실력이슈 ㅠ)

     

    그리고 빠르게 백엔드를 마치고 프론트엔드를 돕고 싶었으나, 게임 개발 특성상 백엔드에서 다뤄야 하는 데이터와 로직이 굉장히 많았고, 이것들을 프로젝트 마지막까지 계속해서 붙잡아야 했다보니 처음에 예상했던 것보다 프론트엔드와 스마트 컨트랙트에 힘을 별로 못 쏟은 것 같아서 그 부분 역시 조금 아쉬웠습니다.

     

    그래도 이런 부분들은 프로젝트가 거듭되고 백엔드에 대한 지식을 늘려나가면서 충분히 해결할 수 있을 것이라고 생각합니다!

     


    1. 기획단계

    이번 NFT 특화 프로젝트에서는 기획을 많이 뒤엎기도 했고, 디자인 작업 및 팀원 변동 이슈때문에 다소 시간이 오래 걸렸습니다!

    NFT 프로젝트라는게 참 어렵더라구요..

     

    1.0. 디자인

    이번에도 디자인 협업에는 Figma가 사용되었습니다!

    한창 디자인작업을 하던 중 Adobe에 Figma가 인수되었다는 무시무시한 소식도 들려왔지만.. ㅋㅋㅋ

    피그마에 메모를 남기는 기능을 알게 되어서 알차게 사용할 수 있었습니다!

     

     

    1.1. ERD

    ERDCloud를 사용하여 간단하게 작성했습니다.

    핑퐁클래스 프로젝트에 비해 훨씬 단순해진 백엔드의 모습을 볼 수 있습니다.

     

    그럼에도 불구하고 예상보다 훨씬 많은 양의 테이블과 컬럼들이 생성되었습니다.

    이 부분 이외에도 게임 처리에 대한 데이터스키마가 존재했기 때문에 어떻게 보면 핑퐁클래스보다 훨씬 더 대규모의 데이터셋을 다룬게 아닐까라고 생각할 수 있을 것 같습니다!

     

     

    1.2. API문서

    일반 API, Socket, SmartContract 세 가지로 나누어서 문서를 작성하였습니다.

    익숙하지 않은 부분이 있어서 계속해서 변경되는 부분들이 있었지만, 이런 식으로 스프레드시트에 잘 정리해두다보니 나중에 개발할 때에는 서로간에 의사소통에 있어서 훌륭한 매개체가 될 수 있었습니다.

     

     

    1.3. Git

    이번에는 Git 전략으로 GitHub-Flow를 사용했습니다!

    기존에 사용했던 Git-Flow의 경우 프로젝트 규모에 비해서 다소 많은 절차가 있다고 판단되어, Develop Branch를 없애고 FE와 BE, SC(Smart Contract)에서 각각 Feature Branch를 파고 바로 master에 합치는 방식을 사용하였습니다.

     

    이런 방식을 사용하다보니 잘못된 내용이 Master Branch에 올라가는 위험성이 증가한다는 단점이 있었지만, 기존보다 훨씬 빠르고 간결하게 Branch Merge를 진행할 수 있다는 장점이 그러한 단점을 충분히 덮을 수 있었습니다.

     

     


    2. 개발단계

    2.0. 기술 스택

    개발 진행 중에도 사소하게 많은 변화를 겪었습니다.

    Spring BootMySQLJPA를 사용한다는 기본 골자는 변하지 않았지만, Redis를 사용하려다가 사용하지 않게 되었습니다. (이 내용에 대해서는 후술합니다!)

     

    그 외에 NginX, Docker, Jenkins의 사용은 이전과 동일했고, 백엔드 서버는 Amazon EC2, 그림 및 사진 데이터 저장은 Amazon S3에서 이루어졌습니다.

     

    블록체인 코드의 경우 Solidity를 기반으로 진행하였고, 테스트는 편의상 Remix환경에서 진행하고 실제 프로젝트 안에서는 Truffle을 사용했습니다.

    이것도 기술 스택이라고 봐야하는 지는 잘 모르겠지만, 블록체인 토큰 및 NFT 저장용 지갑으로는 MetaMask를 사용했습니다. (실제 서비스 내에서의 재화 사용 및 그림의 NFT화도 MetaMask 로그인을 해야 가능합니다)

     

    프로젝트 진행 막판에 블록체인 네트워크 관련 문제들이 많이 발생해서, 프로젝트 후반부에는 제공된 네트워크 서버 대신 Goerli 테스트 네트워크를 사용했습니다.

    이 부분은 다행히 추후에 어느정도 개선이 되었지만, 개발과는 깊은 연관성이 없기 때문에 짧게만 이야기하고 넘어가겠습니다!

     

    프론트엔드는 공통프로젝트에서 사용했던 기술스택과 거의 동일하게 TypeScript 기반의 ReactRedux-Toolkit, 그리고 Emotion을 사용했습니다.

     

     

    2.1. 실제 개발 과정

    실제 개발은 약 3주간 진행되었고, 마지막 1주 정도는 수면 시간을 거의 5시간 이내로 줄여가면서 개발했습니다.

    게임 개발 특성상 실시간성이 강한 요소들이 많다보니 다양한 오류들이 발생하였고, 기존보다 훨씬 적은 인원이 백엔드를 담당하기도 했고 무엇보다 제가 Java를 이용한 백엔드 개발이 사실상 처음이었기 때문에 시행착오가 굉장히 많았습니다.

     

    그래도 개발 초기에 Java와 Spring Boot를 이용한 백엔드 경험이 있는 다른 팀원들에게 많은 도움을 받았고, 개발 막판에 잠을 줄여가면서까지 개발을 진행했기 때문에 정해둔 일정에 크게 벗어나지 않게 개발을 진행할 수 있었습니다.

     

    돌아버린 개발시간?

     

    이번 프로젝트에서는 Discord를 이용하여 화면공유 및 작업 내용을 공유하였고, 모르는 부분이 있으면 실시간 Live, 혹은 VSCode의 LiveShare를 사용하여 질문하거나 오류를 해결했습니다.

     

    2.2. 폴더 구조

    백엔드는 크게 api, config, exception, util로 폴더를 구성했습니다.

     

    api 폴더 내에는 controller, dto, repository, service가 존재합니다.

    controller에는 프론트엔드로부터 받아온 요청들을 처리하는 컨트롤러들을 담아두었습니다.

    dto에는 사용자로부터 받는 요청의 양식, 사용자에게 전달하는 반환값의 양식들을 담아두었습니다.

    repository에는 백엔드단에서 사용하는 Entity와 JPA를 사용하기 위한 repository가 담겨있습니다. (두 요소를 따로 분리할 수도 있었지만, 팀원들과의 협의 하에 하나의 폴더로 뭉쳐두었습니다.)

    service에는 백엔드단에서 사용하는 각종 서비스 로직을 담아두었습니다.

     

    가장 큰 특징이라고 한다면, 백엔드에서 게임 데이터와 관련된 요소를 관리하는데, 관리를 위한 각종 로직들을 RoomRepository에서 처리했습니다. DB를 대신한다는 측면에서 이 곳에 두는 것으로 결정하였는데, 맞는 결정인지는 잘 모르겠으나 개발 과정 중 의사소통에 있어서 크게 문제는 없었기에 변경 없이 그대로 진행하였습니다!

     

     

    프론트엔드는 pages 폴더에 우리 눈에 보이는 화면들을 정리해두고, components 폴더에 실제 내부에서 사용하는 커다란 기능들을 만들었습니다.

    그리고 layout에는 Modal, Header, Footer와 같은 화면 구성 요소들에 대한 내용들을 담았고, Store에는 상태관리를 위한 내용들, Style에는 공통 스타일 요소들을 담았습니다.

     

    추가로 actions에는 API 관련 내용들, assets에는 사진, 음성 및 영상 관련 소스들, contract에는 블록체인 관련 API와 소스들을 담아두었습니다.

     

     

    2.3. 담당 개발 기능

    제가 맡아서 진행했던 부분은 유저와 관련된 백엔드 로직게임과 관련된 백엔드 로직이었습니다.

     

    유저 처리의 경우 일반적인 회원가입 로직과 크게 차이가 없었기 때문에 Spring Boot와 JPA를 공부하고 경험한다는 측면에서 편하게 진행할 수 있었습니다.

     

    한 가지 특별한 점이라고 한다면 MetaMask 정보를 연결하는 부분일텐데, 이 부분도 API 처리가 잘 되어있어서 받아오는 대로 DB에 연결만 해주면 됐기 때문에 크게 무리 없이 진행할 수 있었습니다.

     

     

    가장 문제가 되는 부분이라고 하면 바로 게임 제작이었겠죠..?!

    이 부분에서는 우여곡절이 정말로 많았습니다.

    게임 내 모든 데이터를 관리해야 하는 것은 아주 당연한 것이었는데, 어떻게 관리할까에 대한 고민을 계속해서 하게 되었습니다.

     

    MongoDB를 도입해서 게임 데이터를 별도로 관리하는 방법, Redis에다가 게임 데이터를 저장하는 방법, 백엔드 서버 자체에 게임 데이터를 저장하는 방법이 존재했습니다.

    이 중에서 MongoDB를 도입하는 것은 다소 과하다고 생각되어 Redis를 도입하고자 마음먹고 레퍼런스를 찾아보고 실제로 도입까지 성공했습니다.

     

     

    하지만 또 하나 발생한 문제는, Redis에 모든 데이터를 다 넣기에는 너무나도 복잡한 자료구조였습니다.

    Redis에 자료를 넣게 된다면 매번 꺼내서 파싱을 해야하는 문제가 있는데, 파싱을 하기에는 너무나도 거대한 양의 데이터여서 부담이 됐습니다.

    (Redis를 어떻게든 써보려고 공부하면서 채팅 서비스에서 Redis를 사용하면 지금 개발하고 있는 게임 서비스보다 훨씬 효율적으로 Text 데이터를 저장할 수 있겠다는 생각도 할 수 있었습니다 ㅋㅋㅋㅋ;;)

     

     

    그래서 선택한 것이 백엔드 서버에 직접 저장하는 것이었습니다.

    백엔드의 특정 위치에 자료를 저장해놓고, 내부에 메서드를 만들어서 접근했습니다.

    굳이 static 형태로 선언해서 처리한 이유는 해당 RoomRepository가 아닌 다른 곳에서도 new를 통해 객체를 생성하는 행위를 하지 않고도 바로 접근할 수 있게 만들기 위해서였습니다.

     

     

    이제 RoomRepository 안에 각각의 게임 데이터를 변경할 수 있는 다양한 로직을 메서드 형태로 넣어두었습니다.

    마치 Service단에서 요청을 처리하듯이 비슷한 방식으로 Room이 담긴 HashMap의 자료를 조회하고 변경하는 형태입니다.

    아래는 대표적인 예시로 가져온 방에 접속하는 joinRoom 메서드입니다.

     

     

    이런 로직을 설계한 대로 만들기만 하면 데이터의 저장은 완료가 됩니다.

    하지만 또 다른 어려움이 생겼는데, 그것은 바로 소켓 연결이었습니다.

     

    이전 Sooltreaming 프로젝트에서 node.js와 client간의 socket통신을 socket.io를 이용해서 구현한 적이 있었는데, 이번에는 백엔드 서버가 Java로 구성되었기 때문에 socket.io를 사용할 수 없었습니다.

    그래서 어쩔 수 없이 StompSock.js를 사용해야했죠.

     

     

    어찌어찌 연결은 끝냈으나, 각각이 어떻게 동작하는지에 대해서 알아내는 데에는 꽤나 많은 시간이 걸렸습니다.

    지금 와서 보면 그렇게 어렵지 않은 내용들인데, 아무것도 모르는 상태에서 잘 정리된 한국어 자료도 찾기 쉽지 않았기 때문에 시간이 오래 걸렸던 것 같습니다.

     

    백엔드서버에서 socket요청을 받으면, 그 요청에 맞는 roomRepository 내의 메서드가 작동하여 잘 동작하게 만드는 것이 프로그램의 핵심이었기 때문에 사용자로부터 요청을 받기 위한 SocketController를 만들었습니다.

     

    아래 사진은 MessageMapping 과정을 통해 게임방에 입장할 때 작동하는 코드입니다.

    소켓 기초 url + /room/{sessionId}/join

    로 socket 요청이 들어오면 joinRoom 메서드가 작동하고, sendingOperation.convertAndSend 메서드에 의해서 적절한 데이터가 다시 연결된 socket(방에 있는 모든 사람들)으로 전달되는 방식입니다.

     

    여기서 Stomp를 이용한 소켓 연결 방식과 구조에 대해서 모두 설명하기에는 "프로젝트 리뷰" 라는 포스팅의 핵심에서 많이 벗어날 것 같아서, 보다 자세한 내용은 나중에 Stomp의 작동방식에 대해서 설명할 기회가 있다면 그 때 작성해보도록 하겠습니다!

     

     

    그리고 이렇게 작성한 내용들을 프론트 연결 없이 테스트하기 위해서 크롬 확장프로그램 중에서 Apic이라는 애플리케이션을 사용했습니다.

    매번 새로운 창을 켜고 문자형태로 테스트해야한다는 불편함이 있었으나, 브라우저를 다르게 할 필요가 없었다는 점에서 나쁘지 않게 사용했습니다.

    혹시나 Stomp로 연결한 socket을 브라우저 상에서 프론트엔드 도움 없이 테스트해보고 싶다면 크롬 웹스토어에서 Apic을 사용해보는 것도 좋은 경험이 될 것이라고 생각합니다!!

     

     

    Apic - Complete API solution

    The only tool you will ever need for all you API Design, Documentation and Testing needs.

    chrome.google.com

     

     

    여기까지 마치고 테스트를 돌렸을 때 프로그램은 정상적으로 잘 동작했습니다.

     

    하지만 한 가지 난관이 더 있었는데, 바로 타이머였습니다.

    30초, 혹은 10초가 지나면 턴이 종료되는 부분은 게임 정보이고, 게임 정보는 모두 백엔드에서 관리하고 있어야 한다는 의견에 모두가 동의했기 때문에 현재 남은 시간을 처리하기 위해서 Timer를 사용해야 했습니다.

     

    그래서 하나의 방에는 타이머 변수를 담은 무언가가 필요했고, Room Entity 안에 Timer객체를 넣어서 사용하는 방식을 채택하게 되었습니다.

    이 요소는 프론트엔드에서의 setTimeout과 비슷하게 동작했기 때문에 이해 자체는 크게 어렵지 않았습니다.

    (Redis를 사용했다면 Timer 객체를 별도로 관리해야 할 것 같아서 더욱 어려웠을 것이라고 생각합니다!!)

     

     

    이제 이 타이머변수에다가 게임 내 Phase가 지날 때마다 변경시키고, 현재 Phase 자체를 변경시키는 부분을 담아두었습니다.

    제가 짠 코드가 완벽하지 않은 부분 중 하나인데, 여기서 특정 시간 이후에 작동하도록 만들어놓은 함수가 간혹 오류가 발생하여 제대로 초기화가 되지 않는 경우가 발생했습니다.

    이 부분은 아마도 프론트엔드의 비동기 문제와 동일한 맥락에서 발생한 문제라고 생각됩니다.

     

    SocketController
    RoomRepository 내 startTimer 메서드

     

    타이머 처리까지 완료가 되고 나서는 백엔드에서 제가 맡은 임무가 종료되어서 프론트엔드로 넘어갔습니다.

    (물론 QA과정에서 발생하는 백엔드 에러 수정은 프로젝트 종료시까지 계속되었습니다!)

     

    프론트엔드에서는 메인 페이지, 랭킹 페이지, 게임 내 일부 기능(채팅 등), FAQ 페이지를 맡아서 진행했습니다.

    사용해보고 싶었던 grid도 직접 적용해보았고, 스크롤 처리 문제도 직접 해결할 수 있었습니다.

     

    특히나 인상깊었던 부분이 예전 프로젝트에서도 똑같이 겪었던

    "너비가 넓으면 가운데 정렬하되 맨 앞부터 보이게 하는 문제"

    를 해결한 것이었습니다.

     

    랭킹의 오늘의 NFT 거래 파트에서는 flex를 사용했는데, 어떤 속성을 만져야할 지에 대해서 고민을 많이 했었습니다.

    한참을 고민하고 검색하던 중 StackOverflow에서 같은 고민을 하다가 해결한 분의 메시지를 보고 원하는 대로 구현을 할 수 있었습니다.

    Safe center라는 조건을 처음 알았습니다.. 정말로...

     

     

    Can't scroll to top of flex item that is overflowing container

    In attempting to make a useful modal using flexbox, I found what seems to be a browser issue and am wondering if there is a known fix or workaround -- or ideas on how to resolve it. The thing I'm t...

    stackoverflow.com

     

    Normal Center일 때 모습
    Safe Center일 때 모습

     


    3. 종합

    처음으로 맡는 백엔드 작업이었기 때문에 설레기도 했고, 어렵기도 했습니다만 정말로 재미있게 프로젝트를 진행할 수 있었습니다!

    프로젝트 초반부에 팀원의 수가 줄어들었고, 사정상 프로젝트에 많은 시간을 할애할 수 없는 팀원이 있었기에 팀 내 유일한 풀타임 백엔드 개발자로서 부담감이 다소 늘어났지만, 오히려 '내가 할 부분이 많아졌다는 점에서 재미있겠다' 라고 느낄정도로 열정적으로 프로젝트에 임할 수 있었습니다.

     

    다만 쪼오금 아쉬웠던 점은 위에서 언급한 대로 같이 Backend를 맡게 된 팀원들 중 1명은 온전히 시간을 쏟을 수 없었던 상황이었고, 1명은 프로젝트 사정상 Smart Contract로 할당되어야 했기 때문에 대부분의 문제에 있어서 혼자서 난관을 헤쳐나가야 했던 부분입니다.

    사수의 중요성을 뼈저리게 느낄 수 있었습니다 ㅠㅠ

     

    그래도 Java, Spring Boot, MySQL, JPA 등 프론트엔드 개발자가 쉽게 접하기 힘든 백엔드적인 요소들에 대해서 정말로 많이 부딪히면서 배우게 되었다는 것입니다.

    이번 도전이 아니었다면 또 언제 Spring Boot를 이용해서 이정도 규모의 프로젝트에서 백엔드 부분을 맡아서 진행할 수 있을까요 ㅎㅎ

     

    아쉽게도 본상 수상에는 실패했지만 NFT에 대해서 많이 알게 되었고, 백엔드 개발자가 고민할 수 있는 부분에 대해서도 많이 고민할 수 있었고, 코드리뷰와 주간테크톡을 통해서 학습적인 부분도 충분히 챙겨갈 수 있었기에 정말로 만족스러운 프로젝트였습니다!!

     


    4. 애플리케이션 화면 사진

    게임 내 카드 제출
    게임 내 카드 선택
    게임 결과
    내 카드 확인
    그림 생성
    NFT 카드 거래소
    이달의 그림 투표
    랭킹페이지
    FAQ 페이지

     


    5. 참고 자료 및 기술 스택 관련 자료

     

    GitHub Flow

    Git/GitHub 안내서 - GitHub Flow

    subicura.com

     

    Spring Data JPA

    Spring Data JPA, part of the larger Spring Data family, makes it easy to easily implement JPA based repositories. This module deals with enhanced support for JPA based data access layers. It makes it easier to build Spring-powered applications that use dat

    spring.io

     

    Redis

    Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache, and message broker

    redis.io

     

    Timer (Java Platform SE 7 )

    Schedules the specified task for repeated fixed-rate execution, beginning at the specified time. Subsequent executions take place at approximately regular intervals, separated by the specified period. In fixed-rate execution, each execution is scheduled re

    docs.oracle.com

     

    STOMP

    STOMP is a very simple and easy to implement protocol, coming from the HTTP school of design; the server side may be hard to implement well, but it is very easy to write a client to get yourself connected. For example you can use Telnet to login to any STO

    stomp.github.io

     

    sockjs

    SockJS-node is a server counterpart of SockJS-client a JavaScript library that provides a WebSocket-like object in the browser. SockJS gives you a coherent, cross-browser, Javascript API which creates a low latency, full duplex, cross-domain communication.

    www.npmjs.com

     

    반응형
    • 네이버 블로그 공유하기
    • 네이버 밴드에 공유하기
    • 페이스북 공유하기
    • 카카오스토리 공유하기