4주 프로젝트 - dev log # 13

Joshua Song / 송성현·2020년 2월 19일
0

im_16 프로젝트

목록 보기
17/21

음 일단 한 이틀치를 몰아서 쓰는 것 같은데...그래도 기억이 fresh할때 써보는 것이 좋을 것 같다.
지도에 마커를 찍고 그 같은 데이터로 카루셀들을 렌더하고 있었는데 엄청나게 큰 버그가 생겨 나와 프런트 분 중 한분이 매달려 몇 시간을 고민해봤는데 해결되지 않아...결국 마커 하나를 누르면 그 정보만 pop up 식으로 띄우게 코드가 수정되었다.

이 에러가 참...애매한게 일단 카루셀은 배열의 인덱스를 보고 해당 인덱스의 값을 렌더하는 식으로 기본 설정이 되어있다. 문제는 처음 화면을 잡아 받아올때 마커와 카루셀 배열은 같은 정보와 길이를 가지고 렌더가 된다. 그래서 마커 개수와 카루셀 개수가 같다. 하지만 카루셀을 넘기면서 다른 마커로 이동할 때 화면이 바뀌며 그 화면에 맞는 마커를 가져온다. 그러면 배열의 길이가 바뀌면서 덩달아 인덱스 값이 바뀌어 이미 렌더된 카루셀을 넘기더라도 마커에 맞는 정확한 정보가 보이지 않는다.

예를 들어 2번째 인덱스에 a라는 마커와 카루셀이 있는데 이 카루셀로 넘어가며 새롭게 정보를 받아와 마커 배열의 길이가 6에서 5로 줄고, 가장 앞에 있던 정보가 사라지면 인덱스가 하나씩 앞으로 이동하면서 카루셀의 3번째 인덱스는 더이상 a가 아닌, a 다음 마커가 가지게 된다. 그러면 마커와 카루셀 정보가 맞지 않게 보인다...그리고 넘어가며 길이가 줄면 마지막 순서의 마커는 졸지에 인덱스를 잃어버린거여서 에러가 뜨게 된다. 수많은 방법을 고민해보고 concat을 해서 분리를 할까, 마커와 카루셀 정보를 받아올 때 정보 받아오는 것을 다르게 할까 고민을 하다가 결국 해결책을 찾는데에 실패했다. 일단 카루셀은 배열의 인덱스를 기반으로 작동하는데 화면을 움직일 때마다 새로운 정보를 받아 길이가 바뀌는 특성상 해결하는게 불가능해보였다. 얼마 남지 않은 시간상...아깝지만 팝업으로 구현하기로 했다. 팝업은 선택한 마커 정보를 넘겨주기만 하면 돼 그리 어렵지 않게 구현한 것 같다.

어플에서 포스트의 댓글 부분을 소켓으로 구현하기로 해 소켓의 기본 세팅을 해주고 이후 필요한 부분만 소켓의 기능을 활용하기로 했다. 댓글을 추가할 때만 디비에 넣어주고 그 추가된 내용을 클라이언트에 뿌려주면 된다. 생각보다 코드는 많이 길지 않았지만 흐름을 파악하는데 오래 걸렸고 또 코드가 길지 않았기에 속으로 이정도만 해도 된다고..? 하면서 신기해 했던 것 같다.

소켓을 적용할 부분은 포스트를 들어가고 나올 때, 포스트 아이디 만의 방을 만드는 것이고 또 커멘트를 추가할 때 소켓을 적용해 바로바로 받아서 클라이언트에 뿌려주는 부분이었다.

const PORT : Number = 8000;
const server = http.createServer(api);
const io = require("socket.io")(server);

const post = Post(io);
const comment = Comment(io);

먼저 socket을 import 해와서 사용할 준비를 하고 이미 실행하고 있던 서버에 적용을 해준다. 그리고 post와 comment의 인자로 io를 넣어줘 socket을 적용해준다. post와 comment는 관련 요청들을 처리하는 route 코드로 io를 인자로 받아 처리한다.

io.on("connection", (socket:any) => {
    const { postId } = socket.handshake.query;
    // eslint-disable-next-line no-param-reassign
    socket.postId = postId;
    console.log("User connected to postID,", postId);
    socket.join(postId);

    socket.on("disconnect", () => {
        console.log("disconnected postID: ", postId);
    });
});

index 파일에 socket의 연결 상태를 설정해준다. 먼저 소켓을 적용한 서버와 클라이언트가 연결이 되면 connection이라는 메시지를 받을 텐데 이 메시지를 받을 때 콘솔로 띄우고 포스트 아이디를 보여줘 서버측에서는 어떤 포스트로 접속했는지 볼 수 있다. 보면 소켓을 조인 시켜줘 각 포스트 아디디마다 다른 소켓 방을 분리해주는 원리이고 클라이언트에서 disconnect 요청이 왔을 시에 그 포스트 아이디로 연결되어 있는 방을 끊어준다. 이렇게 연결을 구축했다면, 이제 이 소켓을 적용해야 한다. 서버에서 소켓을 적용해야 한 부분은 일단 댓글을 추가했을 때, 그리고 아예 포스트를 삭제할 때 구현해야 했다. 댓글 삭제부분에서의 소켓은 고민이었던게 중간 댓글을 삭제한다면, 결국 그 아래에 있는 댓글들을 다보내줘야해서 소켓을 사용하는 이유가 없는것 같아 삭제는 그냥 일반 post 요청으로 처리하기로 했다.

router.post("/add", helper(async (req:express.Request, res:express.Response) => {
        const { content, postId }:{content:string, postId:number} = req.body;
        const { accessToken }:{accessToken:string} = req.signedCookies;

        const userId = getUserIdbyAccessToken(accessToken);
        const result:InsertResult = await CommentService.insertComment(postId, userId, content);

        if (result.raw.affectedRows) {
            const newComment:object|undefined = await CommentService.getComment(result.identifiers[0].id);

            io.to(postId).emit("new comment", newComment);
            res.status(201).send("Adding comment was successful");
            return;
        }

        throw new CustomError("DAO_Exception", 409, "Failed to add comment");
    }));

소켓을 적용한 부분은, 댓글 추가 후 추가한 새로운 댓글을 포스트 아이디 방으로 보내는 것이다. 생각보다 간단한 코드이다. 삭제 부분은,

router.get("/disconnect", helper(async (req:express.Request, res:express.Response) => {
        // eslint-disable-next-line camelcase
        const { socket_id } = req.query;
        io.to(socket_id).emit("drop", "");
        res.status(200).send("Successfully disconnected socket");
    }));

    // delete Post
    router.post("/delete", helper(async (req:express.Request, res:express.Response) => {
        const { postId }:{postId:number} = req.body;
        const deletePost:UpdateResult = await PostService.updateState(postId);
        if (deletePost.raw.changedRows === 0) throw new CustomError("DAO_Exception", 409, "Failed to delete post");
        await PhotoService.deletePostPhoto(postId);

        io.to(postId).emit("drop", "");
        res.status(201).send("Successfully deleted post");
    }));

포스트 삭제 시, 그 방을 drop하는 식의 코드를 짰고 이 메시지를 클라이언트에 보내면 그 아이디에 해당하는 방의 연결을 끊어야 하기 때문에 그 메시지를 보내는 걸로 하였다. 그리고 클라이언트에서 그 요청을 받아 소켓아이디 방의 할당된 소켓을 없애려면 그 소켓아이디를 드랍시키는, "/disconnect"라는 부분을 만들었다. 그렇다면 클라이언트에서는 어떻게 작동할까?? 클라이언트의 이 부분은 내가 작성했다.

connectSocket = () => {
    const { cat } = this.root;
    const socket = socketio.connect(`${SERVER_URL}`, {
      query: `postId=${cat.selectedCatPost.id}`,
    });
    const helper = sockets => {
      sockets.emit('new comment', 'hello');

      sockets.on('connect', () => {
        if (socket.connected) {
          this.socketId = sockets.id;
          this.isConnectSocket = true;
        } else {
          console.log('Connection Failed');
        }
      });

      sockets.on('drop', () => {
        console.log('drop');
        sockets.disconnect();
        this.isConnectSocket = false;
      });

      sockets.on('new comment', comment => {
        this.newComment = comment;
        console.log("새로운 커멘트: ", this.newComment);
        this.commentList.unshift(this.newComment);
        console.log("렌더해야하는 목록", this.commentList);
      });
    };
    helper(socket);
  };

위에 코드는 클라이언트 commentstore에 소켓 관련 함수인데 상당히 직관적이다. 소켓연결을 위해 나온 링크에 연결을 하고 고양이 포스트 아이디를 요청의 query에 준다. 그러면 서버에서 이 포스트 아이디로 방을 열 수 있게 된다. 이후 소켓의 메세지에 따라서 comment store의 state 값을 바꾸어 준다.

 offUser = async navigation => {
    const result = await axios
      .get(
        `${SERVER_URL}/post/disconnect/?socket_id=${this.socketId}`,
        defaultCredential,
      )
      .then(res => true)
      .catch(err => {
        this.root.auth.expiredTokenHandler(err, navigation);
        console.dir(err);
      });
    return result;
  };

그리고 또 포스트를 삭제하여 이제 소켓을 닫아야 할때 실행할 함수도 만들어 주어 필요한 컴포넌트에서 적용해주었다.

소켓을 공부하고 적용할때 흐름 파악하는게 어렵고 기능은 화려한데 코드가 생각보다 많치 않아 헷갈렸다. "댓글을 쓰고있습니다" 이런 기능이 없기에 broadcast는 사용하지 않았지만 emit기능을 잘 구현한 것 같고 또 연결과 끊음이 확실한 것 같아 만족스럽다. 커멘트도 렌더가 실시간으로 잘 된다.

현재 단계와 주어진 시간에서 구현할 수 있는 것이 무엇이고 어떤 extent까지 가능한지 빠르게 파악해 시간 낭비를 막는게 매우 중요한 것 같다. 새로운 스택을 사용하는 것도 중요하지만 잘 활용하는 것도 중요하다. 프로젝트 남은 시간동안 서버에서 새롭게 크게 짤 부분은 없지만 있는 코드의 버그를 수정하고 리팩토링이 필요해 보인다.

profile
Grow Joshua, Grow!

0개의 댓글