2021-02-08(Mon)

항목

내용

학습 날짜

2021-02-08(월)

학습 시간

11:00~24:00

학습 범위 및 주제

Websocket

학습 목표

Appearance 채팅차단 마저 구현

동료 학습 방법

sanam, jujeong님과 페어코딩을 진행함

상세 학습 내용

문제 1)

Backbone Model, Collection을 통해서(Backbone에 구현되어있는 메서드를 활용해서) 백엔드와 통신을 주고 받아야할까? 아니면 우리가 구현한 Helper 내의 fetch 함수를 이용해야할까? 지금까지는 이를 재량껏 선택했는데 일관성을 위해 선택 기준이 필요하다.

배경)

  1. 특정 유저를 채팅차단했을 때, 즉 채팅 차단 버튼을 눌렀을 때 클라이언트 사이드와 서버 사이드 모두 해당 유저가 current_user에 의해 차단되었음을 알아야한다. 그래야 프론트엔드에서는 메뉴를 바꿀 수 있고, 서버 사이드에서는 DB를 바꿀 수 있다.

  2. 백엔드에서는 current_user에 의해 차단되었음을 DB에 저장하는 액션을 이미 잘 구현해둔 상태이다. 프론트엔드에서 채팅 차단을 어떻게 저장하고 다른 유저들에게 전파할지 고민해야 한다.

  3. 참고로 기존에는 유저 차단 버튼을 클릭했을 때 view에서 이 이벤트에 반응해서 chatBan 메서드를 실행시키고 Helper.fetch를 통해서 Post 요청을 보내게끔 구현해뒀다.

해결)

해결이라기보단 논의 끝에 만든 협의점은 아래와 같다.

  • 필요하다면 모델과 컬렉션은 만들어야한다. 이를 모델과 컬렉션을 fetch메서드로 땡겨올 수는 있어야한다. 그래야 최신안을 쉽게 쓸 수 있으니까

  • 반면 프론트에서 서버로 변경을 요청하는 것은 Helper.fetch로 Post를 보내게끔 하자. 일관되게.

문제 2)

로그인과 로그아웃 상태 관리 기준을 정해야한다. 이에 맞춰서 구현을 달리해야한다.

  1. 브라우저당 로그인 허용(기존) 기존 구현에서는 쿠키에 encrtyed[:service_id]로 접근하면 유저가 로그인되어있다고 생각한다. 쿠키가 남아있으면 이 유저를 로그아웃시키고 로그인 탭을 열어주는 깔끔한 방식이다. 문제는 한 브라우저가 쿠키를 공유하기 때문에 여러 탭으로 로그인이 되지 않는다. 두번째 탭에서 로그인할 경우 기존 유저를 로그아웃시켜버려야 한다.

  2. 브라우저 탭당 로그인 허용

    이 경우 쿠키는 로그인 과정에서만 사용하고, persist 케이블로 연결할 때만 쿠키의 서비스 아이디를 사용하고, 이후에 쿠키가 초기화되어도 연결이 되어있으면 로그인되어있는 것으로 판단하자. 시작할 때 쿠키 delete 요청을 할 필요가 없어진다.

해결)

해결이라기보단 논의 끝에 만든 협의점은 아래와 같다.

평가시 동시접속은 다른 브라우저를 이용해서 하도록 하자. 브라우저 탭당 로그인을 허용되도록 처리하는 것이 UX상 더 나을 것이나 요구사항에 포함되어있지 않으니 오버디벨롭인 것 같다. 대신 브라우저 탭당 로그인이 허용되도록 처리하는 것은 우선순위를 미뤄서 ver 1.5에서 구현하자.

의문 1) import을 할 때 왜 자동으로 consumer가 생성될까?

import { ConnectAppearanceChannel } from "../channels/appearance_channel";
import consumer from "../channels/consumer";

1번은 appearance_channel에 있는 변수 중 하나인 ConnectAppearanceChannel을 지정해서 가져온다.

반면 2번에서는 딱히 무엇을 가져오겠다고 지정하지 않았지만 잘 작동한다. 왜? 2번은 딱 하나만 준 다는 것이 보장되기 때문이다. export에 default 옵션을 주면 이 파일에서는 딱 하나만 나간다는 것을 보장해준다.

ㅇㅋ 그래서 왜 consumer가 생성이 미리 되어있을까? export할 때 함수를 실행한 결과값이 export되기 때문이다. consumer 파일을 확인해보면

import { createConsumer } from "@rails/actioncable"

export default createConsumer()

위처럼 되어있다.

# action_cable.js
function createConsumer() {
    var url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getConfig("url") || INTERNAL.default_mount_path;
    return new Consumer(url);
  }

그럼 여러 곳에서 import를 해올 때 다른 consumer가 생성될까? 아니다. 모두 같은 consumer를 참조한다. javascript 특성상 import하며 딱 한번 평가되는 특성을 가지고 있기 때문이다.

참고

Rails & javascript 테크닉 한토막

first_or_create

private
  def find_or_create_ladder_match_for(user)
    match = Match.where(matchtype: "ladder", status: "pending").first_or_create
    side = match.users.count == 0 ? "left" : "right"
    card = Scorecard.create(user_id: user.id, match_id: match.id, side: side)
    match.update(status: "progress") if match.reload.users.count == 2
    match
  end

참고

includes

join_table 사용시 includes를 활용하면 최초 DB 접근시 연관 테이블도 함께 불러온다. 덕분에 접근 횟수를 줄일 수 있다.

  def show

    id = params[:id]
    if (params[:for] == "profile")
      user = User.includes(:in_guild, :score_cards, :tournament_memberships).find(id)
      render :json => { user: user.profile }
    else
      user = User.includes(:in_guild).find(id)
      render :json => { user: user.to_simple }
  end

User table에서 랭킹을 매길 때

stat[:rank] = User.order(point: :desc).index(self) + 1

model에서 scope

scope를 정해주기 위해 user.rb model에 아래처럼 정의

scope :for_ladder_index, -> (page) { order(point: :desc).page(page.to_i).map { |user| user.profile } }

# 이후에 users = User.for_ladder_index(params[:page]) 이런 느낌으로 쓴다.

Safe Navigation Operator

참조하는 object가 nil인지 확인하며 동작한다.

def enemy
    self.playing_game&.users&.where&.not(id: self.id)
end

참고

특정 DOM 요소 안에 어떤 요소가 있는지 확인할 때...

...는 아래처럼 메서드를 작성해보자.

return $("#main-view-container").has(view_name).length > 0;

학습 내용에 대한 개인적인 총평

Appearance View를 만드는데 생각보다 시간이 많이 걸렸지만, 다른 모듈을 만들 때 도움이 될 법한 시행착오를 많이한 것 같다. 다른 모듈을 만들 때 시간을 많이 줄여주겠지.

다음 학습 계획

  • Appearance view 구현완료

Last updated