Rails 8 앱에서 멘토-팀 배정을 드래그앤드롭으로 관리하는 보드를 만들었다. Stimulus 컨트롤러 + fetch + 서버 사이드 HTML 교체 방식이었는데, “되는 줄 알았던” 기능들이 프로덕션에서 하나씩 터졌다.


1. Stimulus 컨트롤러가 아예 로드 안 됨

증상

data-controller="mentor-assignment-board"를 붙였는데 드래그가 안 먹는다. 브라우저 콘솔에 에러도 없다.

원인

importmap-rails를 쓰는 프로젝트에서 한 번이라도 rails assets:precompile을 실행하면 public/assets/ 디렉토리가 생긴다. 이후 개발 환경에서도 Rails는 이 정적 파일을 우선 서빙한다.

문제는 precompile 시점에 존재하지 않았던 Stimulus 컨트롤러들이 public/assets/에 없다는 것. Rails가 public/assets/를 먼저 보기 때문에, app/javascript/controllers/에 있는 새 파일을 무시한다.

Importmap skipped missing path: controllers/mentor_assignment_board_controller.js

해결

rm -rf public/assets

개발 환경에서는 public/assets/가 없어야 importmap이 app/javascript/를 직접 참조한다. CI/CD에서만 precompile이 돌아야 한다.

교훈

importmap 프로젝트에서 Stimulus 컨트롤러가 인식 안 되면, 제일 먼저 public/assets/ 존재 여부를 확인하라.


2. 카드 배경색이 페이지 배경과 합쳐짐

증상

멘토 제출 리뷰 페이지에서 카드 헤더가 페이지 배경에 녹아들어 경계가 보이지 않았다. “디자인이 적용 안 된 것 같다"는 피드백.

원인

CSS 디자인 토큰 시스템에서:

  • 페이지 배경: --surface-secondary (#fafaf9)
  • 카드 헤더: --surface-secondary (#fafaf9)

같은 토큰을 두 곳에 썼으니 당연히 구분이 안 된다.

해결

카드 헤더를 --surface-tertiary(#f5f5f4)로 변경하고, 전체 콘텐츠를 --surface-primary(흰색) 카드로 감쌌다.

<!-- 전체를 흰색 카드로 감싸기 -->
<section class="rounded-3xl border p-6"
  style="background: var(--surface-primary); border-color: var(--border-default);">

  <!-- 필터 탭은 tertiary -->
  <div style="background: var(--surface-tertiary);">
    ...
  </div>
</section>

교훈

디자인 토큰은 “이름"이 아니라 “실제 값의 차이"를 확인해야 한다. 토큰 이름이 달라도 값이 같으면 시각적 구분이 없다.


3. iOS 앱 로그인이 계속 풀림

증상

WKWebView 기반 iOS 앱에서 로그인 후 앱을 종료하고 다시 열면 로그인이 풀려있다. 웹 브라우저에서는 문제없다.

원인

Rails 기본 세션 쿠키는 브라우저 세션 쿠키(만료 시간 없음)다. 일반 브라우저는 탭을 닫아도 세션을 유지하지만, WKWebView는 앱 프로세스가 종료되면 세션 쿠키를 삭제한다.

해결

config/initializers/session_store.rb를 만들어 영속 쿠키로 변경:

Rails.application.config.session_store :cookie_store,
  key: "_app_session",
  expire_after: 30.days

expire_after를 지정하면 Set-Cookie 헤더에 Max-Age가 추가되어 WKWebView도 디스크에 쿠키를 저장한다.

교훈

네이티브 앱 래퍼(WKWebView, Android WebView)를 쓸 때는 세션 쿠키의 영속성을 반드시 확인하라. 브라우저와 동작이 다르다.


4. 조건부 렌더링으로 + 버튼이 사라짐

증상

멘토 배정 보드의 각 팀 카드에 “멘토 추가” 버튼(+)을 넣었는데, 특정 팀에서 버튼이 보이지 않는다.

원인

<% addable_mentors = all_mentors.reject { |m| assigned_ids.include?(m.id) } %>
<% if addable_mentors.any? %>
  <!-- + 버튼 -->
<% end %>

“이미 배정된 멘토를 제외한 목록"이 비면 버튼 자체가 사라진다. 그런데 한 멘토가 여러 팀에 동시 배정 가능한 구조였다. 모든 멘토가 해당 팀에 이미 있으면 버튼이 없어지고, 실수로 빼버린 멘토를 다시 넣을 방법이 없어진다.

해결

  • 버튼을 항상 표시하고, 드롭다운에 전체 멘토 목록을 보여주되 이미 배정된 멘토는 체크 표시, 미배정은 점으로 구분:
<%# 항상 표시 — 조건 없음 %>
<div class="relative">
  <button type="button" title="멘토 추가/제거">+</button>
  <div class="dropdown">
    <% all_mentors.each do |mentor| %>
      <% assigned = assigned_ids.include?(mentor.id) %>
      <button data-action="click->board#toggleMentor"
              data-assigned="<%= assigned %>">
        <%= assigned ? "✓" : "●" %> <%= mentor.name %>
      </button>
    <% end %>
  </div>
</div>

Stimulus 액션도 addMentor에서 toggleMentor로 변경. 배정됨 → 클릭하면 제거, 미배정 → 클릭하면 추가:

async toggleMentor(event) {
  const { mentorId, targetTeamId, assigned } = event.currentTarget.dataset
  const isAssigned = assigned === "true"

  // 배정됨 → 제거 (팀→미배정), 미배정 → 추가 (미배정→팀)
  const sourceTeamId = isAssigned ? targetTeamId : ""
  const destTeamId = isAssigned ? "" : targetTeamId

  const response = await fetch(this.urlValue, {
    method: "PATCH",
    headers: { /* CSRF + JSON */ },
    body: JSON.stringify({
      mentor_id: mentorId,
      source_team_id: sourceTeamId,
      target_team_id: destTeamId
    })
  })

  // 서버에서 갱신된 HTML 반환 → innerHTML 교체
  const data = await response.json()
  this.boardTarget.innerHTML = data.html
}

교훈

“추가할 대상이 없으면 버튼을 숨긴다"는 논리가 항상 옳지 않다. 다대다 관계에서는 토글 패턴이 더 유연하다.


5. 권한 조건에서 역할 하나 빠뜨림

증상

관리자는 보이는 “빠른 과제 만들기” 버튼이 멘토에게는 보이지 않는다.

원인

뷰와 컨트롤러 양쪽에서 admin? || mentor_admin?만 체크하고 mentor?를 빠뜨렸다.

# 컨트롤러
def require_admin_or_mentor_admin
  unless current_user.admin? || current_user.mentor_admin?
    redirect_to root_path, alert: "권한이 없습니다."
  end
end
<!-- 뷰 -->
<% if current_user.admin? || current_user.mentor_admin? %>
  <%= link_to "빠른 과제 만들기", quick_new_path %>
<% end %>

해결

양쪽 모두에 current_user.mentor? 추가.

교훈

권한 체크는 뷰와 컨트롤러 양쪽을 반드시 동시에 수정해야 한다. 한쪽만 고치면 버튼은 보이는데 접근 불가이거나, 접근은 되는데 버튼이 안 보이는 상태가 된다.


정리

문제근본 원인해결 시간
Stimulus 미로드public/assets/ 잔재10분
카드 배경 합쳐짐같은 토큰 값 사용15분
iOS 로그인 풀림세션 쿠키 영속성5분
+ 버튼 사라짐조건부 렌더링 함정30분
권한 누락뷰/컨트롤러 불일치5분

하루에 5가지를 만났지만, 각각은 코드 몇 줄 수정으로 해결됐다. 문제는 원인 파악까지의 시간이다. “왜 안 되지?“에서 “아, 이거였네"까지의 간극을 줄이는 게 경험이라는 걸 다시 느꼈다.