Gatherer – Online Collaboration tool 만들기 by 13th warrior (5) – 객채공유
다섯번째 강좌
가장 많이 사용하는 white boardPanel.mxml을 먼저 살펴봅시다. whiteboard 아래쪽에 위치한 ToolDock.mxml에서 설정된 객체가 whiteboard의 drag & drop에 의해서 생성되고 생성된 객체는 이동, 크기변환 등의 기능을 수행할 수 있게 됩니다. ToolDock에서 사각박스를 선택하고 whitelboard에 drag & drop을 하면 mouseDown(), mouseMove(), mouseUp() 이벤트가 차례대로 일어나게 됩니다.(이건 이전 강좌 어디선가에서도 설명한 것 같은데요. -_-;)
1. mouseDown()
91 92 93 94 95 96 97 98 99 100 101 102 103 104 | private function mouseDown(event:MouseEvent):void{ //trace("mouseDown"); startX = drawArea.mouseX; startY = drawArea.mouseY; //if(buttonState == DRAW || LINE){ if(toolDock.getButtonState() == DRAW || LINE){ drawSurface.graphics.moveTo(startX, startY); } trace("target :" + event.target); drawArea.addEventListener(MouseEvent.MOUSE_MOVE, mouseMove); drawArea.addEventListener(MouseEvent.MOUSE_UP, mouseUp); } |
위치를 설정하고 tooldock으로부터 어떤 객체를 그릴 것인지 선책한 후 MOUSE_MOVE, MOUSE_UP 이벤트핸들러를 설정하죠. 지난번에 설명하였듯이 drag & drop의 처음 부분은 항상 저렇게 간단합니다.
2. mouseMove()
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | private function mouseMove(event:MouseEvent):void{ //trace("MouseMove"); var posX:int = drawArea.mouseX; var posY:int = drawArea.mouseY; drawSurface.graphics.lineStyle(0x000000, 2); //switch(buttonState){ switch(toolDock.getButtonState()){ case SELECT: break; case RECTANGLE: drawSurface.graphics.clear(); drawSurface.graphics.lineStyle(0x000000, 2); drawSurface.graphics.drawRect(startX, startY, posX-startX, posY-startY); break; case CIRCLE: drawSurface.graphics.clear(); drawSurface.graphics.lineStyle(0x000000, 2); drawSurface.graphics.drawEllipse(startX, startY, posX-startX, posY-startY); break; case DRAW: drawSurface.graphics.lineTo(posX, posY); linePoints.push({x:posX, y:posY}); break; case TEXT: drawSurface.graphics.clear(); drawSurface.graphics.lineStyle(0x000000, 2); drawSurface.graphics.drawRect(startX, startY, posX-startX, posY-startY); break; case LINE: drawSurface.graphics.clear(); drawSurface.graphics.lineStyle(0x000000, 2); drawSurface.graphics.moveTo(startX, startY); drawSurface.graphics.lineTo(posX, posY); break; case LIGHTPEN: drawSurface.graphics.lineStyle(15, 0xfae61e, 0.25); drawSurface.graphics.lineTo(posX, posY); linePoints.push({x:posX, y:posY}); break; case MEN: drawSurface.graphics.clear(); drawSurface.graphics.lineStyle(0x000000, 2); drawSurface.graphics.drawRect(startX, startY, posX-startX, posY-startY); break; } } |
쓸데없이 길죠? 설정된 객체에 따라서 화면에 마우스가 이동하는 만큼 생성될 객체의 크기를 보여주기 위함입니다. 여기도 별일은 없죠.
3. mouseUp()
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | private function mouseUp(event:MouseEvent):void{ drawArea.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMove); drawArea.removeEventListener(MouseEvent.MOUSE_UP, mouseUp); drawSurface.graphics.clear(); createShape(); } private function createShape():void { var tempNum1:int = 0; var tempNum2:int = 0; var tempShape:ShapeWithoutPoint; tempNum1 = Math.abs(drawArea.mouseX-startX); tempNum2 = Math.abs(drawArea.mouseY-startY); if(tempNum1 > 3 || tempNum2 > 3){ isSmall = false; }else{ isSmall = true; } switch(toolDock.getButtonState()){ case SELECT: break; case RECTANGLE: if(!isSmall) { getShapeOption(); tempShape = new Square(drawArea, userID, connector, startX, startY, drawArea.mouseX-startX, drawArea.mouseY-startY, lineThickness, lineColor, lineAlpha, shapeColor, shapeAlpha); drawArea.addChild(tempShape); entityID = tempShape.getEntityID(); sendMessageToCreateSquare(); } break; case CIRCLE: if(!isSmall) { getShapeOption(); tempShape = new Circle(drawArea, userID, connector, startX, startY, drawArea.mouseX-startX, drawArea.mouseY-startY, lineThickness, lineColor, lineAlpha, shapeColor, shapeAlpha); drawArea.addChild(tempShape); entityID = tempShape.getEntityID(); sendMessageToCreateCircle(); } break; case DRAW: if(!isSmall) { getShapeOption(); tempShape = new Line(drawArea, userID, connector, startX, startY, drawArea.mouseX-startX, drawArea.mouseY-startY, linePoints, lineThickness, lineColor, lineAlpha); drawArea.addChild(tempShape); entityID = tempShape.getEntityID(); sendMessageToCreateLine(); clearArray(linePoints); } break; case TEXT: if(!isSmall) { getShapeOption(); tempShape = new FontBox(drawArea, userID, connector, startX, startY, drawArea.mouseX-startX, drawArea.mouseY-startY, lineThickness, lineColor, lineAlpha, lineColor, lineAlpha, fontName, lineThickness); drawArea.addChild(tempShape); entityID = tempShape.getEntityID(); sendMessageToCreateFontBox(); } //canvas.addChild(new FontBox(canvas, userID, connector, basisX, basisY, posX-basisX, posY-basisY, lineThickness, lineColor, lineAlpha, shapeColor, shapeAlpha, fontName, fontSize)); break; case LINE: if(!isSmall) { getShapeOption(); tempShape = new StLine(drawArea, userID, connector, startX, startY, drawArea.mouseX-startX, drawArea.mouseY-startY, lineThickness, lineColor, lineAlpha); drawArea.addChild(tempShape); entityID = tempShape.getEntityID(); sendMessageToCreateStLine(); clearArray(linePoints); } //canvas.addChild(new StLine(canvas, userID, connector, basisX, basisY, posX-basisX, posY-basisY, lineThickness, lineColor, lineAlpha)); break; case LIGHTPEN: getShapeOption(); tempShape = new LightLine(drawArea, userID, connector, startX, startY, drawArea.mouseX-startX, drawArea.mouseY-startY, linePoints); drawArea.addChild(tempShape); entityID = tempShape.getEntityID(); sendMessageToCreateLightLine(); clearArray(linePoints); break; case MEN: getShapeOption(); tempShape = new Men(drawArea, userID, connector, startX, startY, drawArea.mouseX-startX, drawArea.mouseY-startY, lineThickness, lineColor, lineAlpha); drawArea.addChild(tempShape); entityID = tempShape.getEntityID(); sendMessageToCreateMen(); break; } } |
case 문 덕분에 여전히 깁니다. -_-; 꼭 리팩토링하고 싶은 부분이기도하죠. 어쨋든 이부분이 객체공유의 시작입니다. getShapeOption() 함수는 마우스로 그린 데이터를 복사하는 거라 별로 신경쓰지 않으셔도 됩니다. 그렇게 복사한 후 case문에 따라 객체 타입에 맞춰서 객체를 생성하고 현재 화면에 나타나게 합니다. 그 다음에 sendMessageToXXXXXXX() 요 부분이 중요한 겁니다. 좀 더 자세히 보자면
181 182 183 184 185 186 187 188 189 | case RECTANGLE: if(!isSmall) { getShapeOption(); tempShape = new Square(drawArea, userID, connector, startX, startY, drawArea.mouseX-startX, drawArea.mouseY-startY, lineThickness, lineColor, lineAlpha, shapeColor, shapeAlpha); // 객체를 생성하고 drawArea.addChild(tempShape); // 화면에 표시한 다음 entityID = tempShape.getEntityID(); sendMessageToCreateSquare(); // 공유! } |
공유! 라고 한 저 함수를 살펴보면
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 | public function sendMessageToCreateSquare():void { //trace("sendMessage"); var posX:int = drawArea.mouseX; var posY:int = drawArea.mouseY; var commandData:Object; var event:CommandEvent; //if(isConnected) //{ commandData = {userID:this.userID, entityID:this.entityID, entityType:"square", eventType:CommandEvent.MAKESQUARE_EVENT , startX:startX, startY:startY, entityWidth:posX-startX, entityHeight:posY-startY , shapeColor:shapeColor, lineColor:lineColor, lineThick:lineThickness, lineAlpha:lineAlpha, shapeAlpha:shapeAlpha}; event = new CommandEvent(CommandEvent.SEND_EVENT, commandData); connector.dispatchEvent(event); //} } |
commandData 를 생성하고 CommandEvent 라는 이벤트를 만들어 connector에 알려줍니다. gatherer는 모든 사용자가 같은 화면을 보고 있기를 원했습니다. 실제 현실에서 한 회의실에 한 화이트보드에 펜을 들고 회의를 한다면 당연한 이야기지요. 그걸 구현하려고 하니 모든 사용자가 같은 화면을 보고 있어야 합니다. whiteboardPanel에서는 그저 화면에 그리는 부분을 담당하고 공유시키는(통신하는) 부분은 Connector라는 클래스가 담당하게 됩니다. dispatchEvent() 함수를 호출하면 해당 객체는 넘겨받은 이벤트가 발생하지요. Flex의 이벤트 구조상 이벤트 핸들러가 있다면 어떤 인스턴스라도 이벤트가 일어날 수 있는 구조를 갖고 있습니다. 또 꼭 이벤트 구조를 활용한 이유는 각 클래스간의 커플링을 낮추기 위함인데, gatherer에 사용된 모든 객체는 UIComponent를 상속해서 만들었고 따라서 EventDispatcher의 자식 클래스이기도 합니다. 결국 모든 객체들은 이벤트를 받을 수 있다고(.dispatchEvent() 함수가 존재한다고) 가정한 것이지요.
또한 중요한 점은 전달/발생하는 이벤트는 제가 정의한 CommandEvent 클래스라는 점입니다. CommandEvent는 객체간의 통신 데이터를 래핑한 수준인데요, 객체에 필요한 동작(함수)이나 데이터를 이 클래스의 인스턴스에 포장해서 보내는 것이죠. 받는 측에서도 전송받은(공유받은) 데이터를 토대로 CommandEvent의 인스턴스를 생성하여 필요한 객체에 전달해주면 됩니다. 그렇게 되면 전달하는 객체는 전달받을 객체의 내부구조를 전혀 알 필요가 없이 그냥 전송하는 것으로 책임을 다 하게 되며, 동작에 대한 책임은 해당 전달받은 객체가 지게 됩니다.

간단히 표현하면 위와같은 형태가 될 것입니다.
Connector 클래스와 CommandEvent 클래스를 살펴보면 좀 더 이해가 잘 갈 것입니다.
Connector.as 파일을 열어보세요. 경로는 PROJECT_ROOT/src/com/gatherer/network/Connector.as 입니다. SharedObject를 위한 NetConnection에 대한 부분이 있구요, 데이터 전송에는 sendHandler()가, 수신에는 syncEventHanlder()가 있습니다.
93 94 95 96 97 98 99 100 101 102 | public function sendHandler(event:CommandEvent):void { send(event.command); } private function send(command:Object):void { //so.setProperty("command", command); connection.call("sendCommand", null, this.userID, command); } |
엄청 간단하죠? 데이터를 전송한다는 게 꽤 어렵다고 생각될 수 있는데 gatherer 소스는 꽤나 간단합니다. SharedObject.setProperty()와 NetConnection.call()이 거의 비슷한 일을 수행하는데요, userid를 전송하는 부분이 채팅 부분과 결합되어 있어서, 해당 데이터를 서버에서 핸들링하려고하다보니 NetConnection.call()을 사용하게 되었습니다.
하는 역할이라곤 command를 서버에 전송하는 것이고, 서버에서도 전송받은 command라는 데이터를 각 클라이언트에게 broadcast하는 것에 지나지 않습니다. command라는 데이터는 위의 “공유!”라고 써있는 부분을 설명한 whiteboardPanel.sendMessageToCreateSquare()에서 commandData입니다.
commandData = {userID:this.userID, entityID:this.entityID, entityType:"square", eventType:CommandEvent.MAKESQUARE_EVENT , startX:startX, startY:startY, entityWidth:posX-startX, entityHeight:posY-startY , shapeColor:shapeColor, lineColor:lineColor, lineThick:lineThickness, lineAlpha:lineAlpha, shapeAlpha:shapeAlpha}; event = new CommandEvent(CommandEvent.SEND_EVENT, commandData); connector.dispatchEvent(event);
commandData 또한 Object 클래스의 인스턴스일 뿐입니다. Flex에서 Object 클래스는 꽤나 특이한 구조를 갖고 있습니다. JSON과 같이 key:value 페어의 데이터를 계속 셋팅할 수 있습니다. 그래서 위와 같이 userID:this.userID 로 셋팅할 수 있는 것이죠. 문제는 해당 데이터를 핸들링하기 위해서는 데이터 구조를 잘 알고있어야 한다는 점입니다. 그것을 좀 더 자동화한 구조를 계획했었으나 구현하지 못했죠. 그래서 저 장황한 case 문을 쓰게 된 겁니다. 만약 그냥 데이터를 전달한다는 생각만 한다면 굳이 저렇게 구성하지 않고 배열이나 벡터와 같은 자료구조에 데이터를 넣어서 필요한 함수를 호출하면 땡이지만, 그러면 해당 클래스 또는 함수들은 호출->호출 등 아주 확실하게 결합하게 됩니다. gatherer에서의 목표는 결합도를 낮추는 것이였지요. CommandEvent를 찾아보면 해답에 근접하게 될 것입니다.
CommandEvent.as 파일을 살펴봅시다. 경로는 PROJECT_ROOT/src/com/gatherer/event/CommandEvent.as 입니다. 정말 간단히 되어 있는데 사실 Flex에서 이벤트 객체가 저렇게 간단합니다. -_-; 버블링을 한다면 좀 더 복잡해지겠지만, gatherer 특성상 버블링은 일어나지 않는게 좋습니다. 잘못했다간 무한 이벤트 전달이 일어나거든요. static으로 선언된 수많은 이벤트 타입을 정의한 부분을 보실 수 있습니다. whiteboard에 필요한 기능들을 총망라한 것이라고 보면 됩니다. 한 이벤트로 동작들을 표현하기 위함입니다. 이것으로 모든 객체는 같은 이벤트 객체를 전달받을 수 있고 전달받은 객체가 그 이벤트 타입이 있으면 동작하고 없으면 무시됩니다. 에러메세지 같은 건 나타나지 않지요. 그게 포인트입니다. -_-
데이터를 수신하는 부분을 살펴보면 또 한걸음 명확해질 겁니다. 다시 Connector.as로 돌아가서 수신하는 쪽을 살펴봅시다. 계속 이야기하지만 gatherer에서는 통신을 SharedObject로 구현했으며 SharedObject는 broadcast로 동작합니다. 이것은 데이터를 송신하면 수신이 자동적으로 일어난다는 겁니다. A객체가 데이터를 송신하면 해당 연결을 갖고 있는 객체 A, B, C, D.. (A도 포함입니다!)가 데이터를 수신합니다. 꽤 빠릅니다. 문제는 A가 보낸 데이터를 바로 A가 수신한다는 것인데요, 그건 데이터에 포함된 userid로 구분하는 것을 구현하면 됩니다. Connector.as에서
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | public function syncEventHanlder(event:SyncEvent):void { var i:Number; trace("sync first"); if(isFirst == true) { if(so.data.commandArray != null){ trace("sync first2"); commandArray = so.data.commandArray; if(commandArray != null){ for(i=0;i<commandArray.length;i++){ trace("initDraw"); receive(commandArray[i]); } } } isFirst = false; } var command:Object = so.data.command; trace("sync: "+this.userID); if(command != null && command.userID != this.userID) { trace("OnSync======================================================="); trace("command.userID:"+command.userID); trace("this.userID:"+this.userID); receive(command); } } private function receive(command:Object):void { //receiver Owner.dispatchEvent(new CommandEvent(command.eventType, command)); } |
이 부분이 데이터를 수신하는 부분입니다. SharedObject에 SyncEvent의 핸들러를 설정하면 저절로 호출되겠죠. syncEventHanlder()는 자신의 아이디를 확인해서 자신이 보낸 데이터가 아니라면 receive()를 호출합니다. receive() 함수라고 복잡하지는 않습니다. 딱 한줄. -_- 전송받은 데이터를 그대로(진짜 그~대~로) CommandEvent의 인스턴스로 만들어서 Connector 클래스 인스턴스를 소유하고 있는 객체에 이벤트로 보내버립니다. 그게 끝이죠. 소유자라고 해봐야 whiteboardPanel입니다. 꽤 단순해 보이지만 저 단순한 구조 덕분에 클래스간의 결합도는 팍~~~~~~~~~~ 떨어지게 되었습니다. whiteboardPanel이 저 CommandEvent를 받아도 할 건 거의 없습니다. 보시면 압니다.
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | public function init():void{ super.positionChildren(); drawSurface = new UIComponent(); drawArea.addChild(drawSurface); drawArea.addEventListener(MouseEvent.MOUSE_DOWN, mouseDown); linePoints = new Array(); connector = new Connector(drawArea); connector.connect("rtmp://192.168.10.99/board", userID, roomName); /* drawArea.addEventListener(CommandEvent.MAKESQUARE_EVENT, makeSquareHandler); drawArea.addEventListener(CommandEvent.MOVE_EVENT, moveSquareHandler); drawArea.addEventListener(CommandEvent.DELETE_EVENT, deleteSquareHandler); drawArea.addEventListener(CommandEvent.RESIZE_EVENT, resizeSquareHandler); drawArea.addEventListener(CommandEvent.ROTATE_EVENT, rotateSquareHandler); */ drawArea.addEventListener(CommandEvent.MAKESQUARE_EVENT, makeSquareHandler); drawArea.addEventListener(CommandEvent.MAKECIRCLE_EVENT, makeCircleHandler); drawArea.addEventListener(CommandEvent.MAKELINE_EVENT, makeLineHandler); drawArea.addEventListener(CommandEvent.MAKELIGHTLINE_EVENT, makeLightLineHandler); drawArea.addEventListener(CommandEvent.MAKESTLINE_EVENT, makeStLineHandler); drawArea.addEventListener(CommandEvent.MAKEFONTBOX_EVENT, makeFontBoxHandler); drawArea.addEventListener(CommandEvent.MAKEMEN_EVENT, makeMenHandler); drawArea.addEventListener(CommandEvent.MOVE_EVENT, squareHandler); drawArea.addEventListener(CommandEvent.DELETE_EVENT, squareHandler); drawArea.addEventListener(CommandEvent.RESIZE_EVENT, squareHandler); drawArea.addEventListener(CommandEvent.ROTATE_EVENT, squareHandler); drawArea.addEventListener(CommandEvent.CHANGETEXTCONTENT_EVENT, squareHandler); } |
init() 함수에는 CommandEvent 핸들러가 꽤 많이 지정되어 있습니다. 저 이벤트 핸들러가 호출되는 건 아까 Connector.receive()에서 이벤트를 전달해주면 자동 호출되는데 전송받은 데이터 안에 eventType도 들어있기 때문에 whiteboardPanel에서 어떤 이벤트인지 알 필요는 없습니다. 그저 알맞는게 호출되는 거지요. 호출되는 핸들러 또한 간단합니다. 이벤트 안에 데이터가 다 들어있으니까요. makeSquareHandler() 요거 하나만 살펴볼까요?
455 456 457 458 459 460 461 | public function makeSquareHandler(event:CommandEvent):void { var commandData:Object = event.command; trace("draw: "+commandData.userID); createReceivedShape(RECTANGLE, drawArea, this.userID, commandData.startX,commandData.startY, commandData.entityWidth, commandData.entityHeight, commandData.lineThick, commandData.lineColor, commandData.lineAlpha, commandData.entityID, commandData.shapeColor, commandData.shapeAlpha); } |
전송받은 데이터로 객체를 생성하는 겁니다. 끝이죠. 만약에 데이터가 전달받을 목적지가 whiteboardPanel이 아니라 이미 생성되어 있는 네모나 동그라미 같은 객체라고 하더라도 어려울 것은 없습니다. 해당 객체에 전달받은 이벤트를 그대로 또 전달해주기만 하면 되거든요.
지금까지 설명한 것이 gatherer의 핵심입니다. 데이터를 주고 받고 그것을 토대로 객체들을 핸들링 하는 것이지요. 내용을 정리하면 각 역할별로 객체를 할당했고 서로 호출하는 것이 아니라 이벤트를 발생시키며, 이벤트 핸들러가 있으면 동작할 것이고 아니어도 에러나지 않습니다.
이번 강좌는 여기서 마치겠습니다. 다음은 보드 위에 그려지는 객체를 구현하는 방법에 대해서 설명하겠습니다.
