본문으로 건너뛰기

NextJS 14.0.2 Server Action 로그인 시 cookie가 저장되지 않는 문제

· 약 2분

◾️ 문제상황

클라이언트: NextJS 14.0.2 서버: ExpressJS

express-session와 passport를 사용해서 서버의 세션로그인을 구현했으며, NextJS의 서버액션을 사용하여 클라이언트에서 로그인을 구현했다.

포스트맨에서 로그인 시 쿠키를 정상적으로 저장하는 반면, NextJS에서 로그인 시, session 쿠키가 브라우저에 저장되지 않는다.

◾️ 문제원인

로그인 시 NextJS의 서버액션(authenticate)을 사용하고 있는데, 때문에 Next(server)에만 쿠키가 저장되고 Next(client)에는 쿠키가 저장되지 않게 된다.

◾️ 해결방안

import NextAuth from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import { authConfig } from './auth.config';
import { cookies } from 'next/dist/client/components/headers';

export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = NextAuth({
...authConfig,
providers: [
Credentials({
async authorize(credentials) {
if (credentials) {
const response = await fetch(`${API ... }/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: credentials.username, password: credentials.password }),
});
const data = await response.json();
// set Next cookie
const cookieList = response.headers.getSetCookie().map((v) => v.slice(0, v.indexOf(' ') - 1).split('='));
cookieList.forEach((v) => cookies().set(v[0], decodeURIComponent(v[1])));
return data.data;
}
return null;
},
}),
],
});

cookies().set(v[0], decodeURIComponent(v[1])) 와 같이 Next의 cookies를 사용해서 Next(server)에서 Next(client)로 쿠키를 직접 저장해주도록 하였다.

iOS React 웹뷰에서 스크롤 후 뒤로가기 시 발생하는 렌더링 오류

· 약 4분

◼︎ 문제상황

  1. iOS 웹뷰 환경의 리액트 특정 페이지에서 스크롤 후 뒤로가기 시, 스크롤 한 만큼의 영역에서 흰 화면이 나타나는 현상
  2. 조금이라도 스크롤을 하면 정상적으로 돌아옴
  3. 강제로 렌더링을 시도하거나 style을 변경해도 문제가 해결되지 않음

◼︎ 문제원인

뒤로가기 시, 기존의 스크롤 위치로 돌아오는 과정에서 페이지가 완전히 렌더링되지 않아 발생한 문제이다. 문제가 발생하는 페이지의 공통점은 로딩 이후 불러온 콘텐츠를 보여주는 방식이었고, Android는 뒤로가기 시 스크롤 위치를 불러오지 않기 때문에 Android 에서 해당 오류가 발생하지 않는 것이었다.


◼︎ 해결방안

1. 스크롤의 높이를 저장하지 않는다.

window.history.scrollRestoration = 'manual'; 을 사용하여 페이지 이동 시 스크롤의 위치를 저장하지 않도록 할 수 있다.
다만 특정 페이지에서만 일어나는 현상으로, window.scrollTo({top:0.1}); 와 같이 스크롤을 초기화 하는 방식을 사용하는 것을 권장한다. 0이 아닌 0.1을 사용하는 이유는 초기 렌더링 시에는 이미 scrollY가 0이기 때문에 scrollTo가 최적화되어 발생하지 않기 때문이다.

2. 페이지가 모두 렌더링 된 후 스크롤을 이동시킨다.

직접 스크롤 저장방식을 구현한 뒤, 목록을 불러오는 시점에 맞춰 스크롤을 이동시킨다.
다만 무한스크롤과 같이 사용자의 동작에 따라 스크롤 높이가 바뀌는 상황에서는 동작할 수 없으며, 별도의 로직이 필요하다.

아래는 스크롤 위치를 저장하고 불러올 수 있는 커스텀 훅이다.

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

/**
* 스크롤 시마다 현재 스크롤 위치를 저장하고, 원하는 시점에 스크롤 위치를 불러올 수 있도록 하는 훅입니다.
* 선언한 컴포넌트에서 스크롤 할 때마다 현재 스크롤 위치를 저장합니다.
* @param ref 해당 컴포넌트가 렌더링 혹은 상태 변경 시 스크롤 위치를 불러옵니다.
* @returns scrollRestore 실행 시 스크롤 위치를 불러옵니다.
*/
export default function useScrollRestoration(
ref?: React.RefObject<HTMLElement>
) {
const { pathname } = useLocation();

// 저장된 스크롤이 0이 아닐 때 스크롤을 불러온다.
const scrollRestore = () => {
if (getScrollStore(pathname)) {
window.scrollTo({ top: getScrollStore(pathname) });
setScrollStore(pathname, 0);
}
};

useEffect(() => ref && scrollRestore(), [ref]);

useEffect(() => {
let currScrollY = 0;
const scrollSaveEvent = () => {
currScrollY = window.scrollY;
};
window.addEventListener('scroll', scrollSaveEvent);
return () => {
setScrollStore(pathname, currScrollY);
window.removeEventListener('scroll', scrollSaveEvent);
};
}, [pathname]);

return { scrollRestore };
}

const getScrollStore = (pathname: string): number => {
const item = window.sessionStorage.getItem('scrollStore');
if (!item) return 0;
const scrollStore = JSON.parse(item);
return scrollStore[pathname] || 0;
};

const setScrollStore = (pathname: string, scrollY?: number) => {
const prevStore = window.sessionStorage.getItem('scrollStore') || '{}';
const store = JSON.parse(prevStore);
store[pathname] = scrollY ?? window.scrollY;
window.sessionStorage.setItem('scrollStore', JSON.stringify(store));
return store;
};

Next.js 14 App router mysql Too many connections 오류

· 약 4분

◼︎ 문제상황

Next.js를 사용하여 개발하던 중 MySQL 데이터베이스와의 통신 과정에서 "Too many connections" 오류가 발생했습니다.

해당 오류는 MySQL 데이터베이스 서버의 동시 연결 수 제한 초과로 인한 오류로, 일반적으로 connections이 설정한 수보다 많거나, 쿼리 실행 후 connection을 반환하지 않아 생기는 문제입니다.

하지만 connections는 매우 넉넉한 설정이었고, 쿼리 실행 후 Connection을 반환했음에도 문제가 계속해서 발생했습니다.


◼︎ 문제원인

Next.js ver.14는 개발 모드에서 API 경로를 매번 재구축하여 데이터베이스의 pool을 생성할 때 마다 트리거합니다. 이때 생성한 pool의 대한 경로가 달라져 connection이 정상적으로 반환되지 못해 발생하는 오류입니다.


◼︎ 해결방안

node 전역변수에 pool 경로를 선언하여 pool을 사용할때 마다 API 경로가 바뀌지 않도록 하여, 정상적으로 반환할 수 있도록 구현하였습니다.

import mysql, { Pool } from 'mysql2';
/**
* check, globalObject, registerService
* Next.js는 개발 모드에서 API 경로를 지속적으로 재구축하는데, initFn()의 경로를 전역으로 지정하여 변경되지 않도록 합니다.
*/
function check(it: false | (Window & typeof globalThis) | typeof globalThis) {
return it && it.Math === Math && it;
}
const globalObject =
check(typeof window === 'object' && window) ||
check(typeof self === 'object' && self) ||
check(typeof global === 'object' && global) ||
(() => {
return this;
})() ||
Function('return this')();

const registerService = (name: string, initFn: any) => {
if (process.env.NODE_ENV === 'development') {
if (!(name in globalObject)) {
globalObject[name] = initFn();
}
return globalObject[name];
}
return initFn();
};

export let pool: Pool;

pool = registerService('mysql', () =>
mysql.createPool({
...config,
})
);

node의 전역변수 globalThis에 접근하기 위해서는 registerService를 사용하여 globalObject에 pool을 선언해야 하는데, 이때 globalThis의 polyfill을 위해 globalObject의 타입체크를 거칩니다.

위와 같은 과정으로 globalObject에 pool을 등록했다면, 서버 렌더링 시 globalObject에 pool의 존재여부를 확인하고, pool이 존재할 경우 해당 pool을 사용하여 API 경로가 바뀌지 않게 됩니다.



참고자료: 카카오페이지 웹 react 포팅 후기 Fix "too many connections" errors with database clients stacking in dev mode with Next.js