YouTube 채널 연결/해제 버튼이 Sidebar(데스크톱)와 BottomNav(모바일) 두 군데에 있는데, 로직이 각자 따로 구현돼 있었다. useYoutubeConnect 훅 하나로 합치기로 했다.
export function useYoutubeConnect() {
const router = useRouter();
const { selectedChannelId, clearSelectedChannelId } = useChannelStore();
const { data: session } = useSession();
const isConnected = Boolean(selectedChannelId);
function openYoutubeConnect() {
const userId = session?.user?.id ?? '';
const base = process.env.NEXT_PUBLIC_API_URL ?? '';
const url = `${base}/auth/youtube?userId=${encodeURIComponent(userId)}`;
const width = 500;
const height = 620;
const left = Math.round(window.screenX + (window.outerWidth - width) / 2);
const top = Math.round(window.screenY + (window.outerHeight - height) / 2.5);
window.open(url, 'youtube-connect', `popup,width=${width},height=${height},left=${left},top=${top}`);
}
async function handleYoutubeClick() {
if (!isConnected) {
openYoutubeConnect();
return;
}
if (!window.confirm('채널 연결을 해제하시겠습니까?')) return;
await apiDelete(`/channels/${selectedChannelId}`).catch(() => {});
clearSelectedChannelId();
router.push('/');
}
return { isConnected, handleYoutubeClick };
}
옮기면서 보니 BottomNav 쪽이 아예 다르게 구현돼 있었다. 연결 해제 흐름(confirm → DELETE → 초기화)이 없고, 클릭하면 그냥 window.location.href로 풀 리다이렉트만 걸려 있었다. userId 파라미터도 안 붙어있었다.
-const YOUTUBE_CONNECT_URL = `${process.env.NEXT_PUBLIC_API_URL ?? ''}/auth/youtube`;
-
-function openYoutubeConnect() {
- window.location.href = YOUTUBE_CONNECT_URL;
-}
+import { useYoutubeConnect } from '@/hooks/useYoutubeConnect';
export function BottomNav() {
- const isConnected = Boolean(selectedChannelId);
+ const { isConnected, handleYoutubeClick } = useYoutubeConnect();
모바일에서는 연결 해제가 아예 안 됐던 거다. 훅으로 합치는 김에 같이 잡혔다.
채널 페이지에서 스케줄러 on/off 토글을 만졌는데, 저장 요청이 실패하면 UI는 이미 바뀐 상태로 남아있는 문제가 있었다. 실패 시 이전 값으로 롤백하도록 고쳤다.
const prevEnabledRef = useRef(enabled);
// ...
onError: () => {
const prev = prevEnabledRef.current;
setEnabled(prev);
stateRef.current.enabled = prev;
setSaveStatus('error');
saveTimerRef.current = setTimeout(() => setSaveStatus('idle'), 3000);
},
여기서 두 가지가 더 걸렸다. 하나는 saveStatus가 error → idle로 바뀌는 순간 텍스트가 "저장 실패"에서 "✓ 저장됨"으로 먼저 바뀐 다음 opacity 트랜지션이 걸려서, 사라지기 직전에 "저장됨"이 잠깐 비쳤다. 직전 상태를 lastSaveStatusRef에 저장해서 idle 전환 중에도 텍스트를 유지하도록 했다.
const lastSaveStatusRef = useRef<'saved' | 'error'>('saved');
// ...
{lastSaveStatusRef.current === 'error' ? '저장 실패' : '✓ 저장됨'}
근데 텍스트는 고쳤는데 색상은 그대로 saveStatus === 'error' 기준으로 분기하고 있어서, idle로 넘어가는 순간 텍스트는 "저장 실패"인데 색은 초록으로 바뀌는 우스운 상태가 됐다. 색상도 같은 ref로 통일했다.
-<p className={cn(..., saveStatus === 'error' ? 'text-red-400' : 'text-green-400')}>
+<p className={cn(..., lastSaveStatusRef.current === 'error' ? 'text-red-400' : 'text-green-400')}>
이 저장 실패 자체는 API 쪽 EventBridge 권한 문제였는데, 그건 다음 글에서.