모듈 최상단 ref 때문에 다른 사용자 정보가 보인 사고
문제 발생
로그인한 사용자 정보를 전역으로 쓰려고 모듈 최상단에 ref를 두었습니다.
// composables/useUser.ts
export const user = ref(null); // ❌로컬에서는 잘 돌았는데, 운영에 올리자 다른 사람의 이름이 보인다는 제보가 들어왔습니다. 새로고침하면 바뀌기도 했습니다.
원인 분석
서버에서 이 모듈은 프로세스당 한 번 평가됩니다. 그 뒤 들어오는 모든 요청이 같은 ref 하나를 공유합니다. A 사용자의 요청이 값을 채우고, 곧바로 B 사용자의 요청이 렌더링되면 B의 HTML에 A의 값이 들어갑니다.
Nuxt 공식 문서가 이걸 경고로 못박습니다 — <script setup>이나 setup() 함수 밖에서 const state = ref()를 정의하지 마십시오. 그리고 이유를 그대로 적어 두었습니다: export myState = ref({})처럼 하면 서버에서 요청 간에 상태가 공유되고 메모리 누수로 이어질 수 있습니다.
useState는 이 문제를 위해 있는 API입니다. 문서의 정의대로 SSR 친화적인 ref 대체품이며, 값이 서버 렌더링 이후(클라이언트 하이드레이션 동안) 보존되고 같은 키를 쓰는 모든 컴포넌트가 공유합니다. 요청마다 격리된 인스턴스를 갖습니다.
해결 방안
useState로 바꿉니다. 키가 공유의 단위입니다.
// composables/useUser.ts
export const useUser = () => useState("user", () => null);<script setup>
const user = useUser();
</script>-
문서가 권하는 형태가 이 래퍼입니다 —
const useX = () => useState('x'). 모듈 최상단에서useState를 바로 호출하지 않고 함수로 감싸는 이유는, 호출 시점이 각 요청의 setup 안이 되어야 하기 때문입니다. -
초기값은 팩토리 함수로 줍니다. 객체나 배열을 값으로 직접 주면 그 인스턴스가 공유될 여지가 생깁니다.
const filters = useState("filters", () => ({ keyword: "", page: 1 }));-
직렬화되는 값만 담습니다.
useState의 값은 서버에서 클라이언트로 payload에 실려 전달됩니다. 함수, 클래스 인스턴스,Map처럼 그대로 옮길 수 없는 값은 여기에 두지 않습니다. -
모듈 스코프에 두어도 되는 것과 아닌 것을 구분합니다. 요청마다 달라지지 않는 상수, 순수 함수, 설정값은 최상단에 있어도 됩니다. 문제는 요청마다 달라지는 가변 상태입니다.
export const PAGE_SIZE = 20; // 괜찮음
export const formatDate = (d) => ...; // 괜찮음
export const currentUser = ref(null); // 위험- 로컬에서는 거의 재현되지 않습니다. 혼자 개발하면 동시에 두 요청이 겹칠 일이 없어서 증상이 안 보입니다. 이 종류의 버그는 코드를 읽어서 막아야 합니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.