안녕하세요 개발하는 토마토입니다 :) 거의 12월 말부터 기획을 시작으로 팀프로젝트를 진행해오고 있었는데, 오늘 하루종일 디버깅을 하던 중 유의미하게 알게된 점들이 있어 블로그에 기록해두려고 합니다!
1. use client
클라이언트 컴포넌트로 렌더링하기 위해 import 문 가장 위에 명시해주면 해당 컴포넌트는 클라이언트 렌더링
(1) 컴포넌트가 ‘use client’ 지시어를 포함한 모듈 안에서 정의되었을 때.
(2) ‘use client’ 지시어가 포함된 모듈의 직간접적인 의존성인 경우
* 여기서 직간접적인 의존성이란?
use client로 클라이언트 렌더링이 되고 있는 모듈을 import 한 경우
ex) ComponentA.tsx
"use client"
export default function ComponentA () {
...
}
ex) ComponentB.tsx
import Component A from './ComponentA';
export default function ComponentB() {
...
}
이런식으로 Component A 는 이미 클라이언트 컴포넌트로 렌더링되고 있었는데, ComponentA를 사용하는 Component B 도 클라이언트 컴포넌트로 간주된다는 것이죠 !
제가 겪었던 문제는,, useState를 쓰고 있는 모듈들에서 use client를 쓰지 않고 있었기 떄문... ㅎㅎ 생각보다 useState를 쓰는 컴포넌트가 많기 때문에 요건 주의해야 겠어요,,
You're importing a component that needs `useState`.
This React hook only works in a client component.
To fix, mark the file (or its parent) with the `"use client"` directive.
2. 두 컴포넌트에서 공통으로 하나의 함수를 사용하는 경우 타입은?
에러 메시지 ㅎ...
Type 'Dispatch<SetStateAction<{ username: string; email: string; }>>' is not assignable to type 'Dispatch<SetStateAction<ErrorState>>'.
Type 'SetStateAction<ErrorState>' is not assignable to type 'SetStateAction<{ username: string; email: string; }>'.
Type 'ErrorState' is not assignable to type 'SetStateAction<{ username: string; email: string; }>'.
Type 'ErrorState' is not assignable to type '{ username: string; email: string; }'.
Types of property 'username' are incompatible.
Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.

제목만 봐서는 잘 이해가 되지 않을 것 같아 부가 설명을 남기겠습니다..
'이메일 인증' 로직을 담고 있는 emailHandler 를 두 곳에서 사용하고 있습니다!
(1) 회원가입 페이지(signup > page.tsx)
(2) 비밀번호 변경 페이지(change-pw > emailVerification.tsx)
각 컴포넌트에서 서버에 api 호출 시 필요한 데이터는 다음과 같습니다
회원가입 : name, nickname, id, password, passwordConfirm, email, phone_number
interface ErrorState {
name: string;
nickname: string;
id: string;
email: string;
password: string;
passwordConfirm: string;
phone_number: string;
}
(변수명 케이스가 왜 그런가 싶겠지만,, 우선 넘어가 주세용)
비밀번호 변경: username, email
* username, email도 모두 string 이지만 따로 interface를 두고 있지 않던 상황
이 상태에서 emailHandler.handleEmailButtonClick 이라는 함수에 전달되고 있던 props의 타입이 다음과 같이 정의 되고 있었어요
emailHandler 함수
export const emailHandler = {
handleEmailButtonClick: async (
e: React.MouseEvent<HTMLButtonElement>,
props: EmailHandlerProps
) => {
e.preventDefault();
if (props.cooldown > 0) {
alert(`${props.cooldown}초 후에 다시 시도해주세요.`);
return;
}
interface EmailHandlerProps {
formData: {
email: string;
find_password: boolean;
};
setErrors: React.Dispatch<React.SetStateAction<ErrorState>>;
setIsEmailSending: React.Dispatch<React.SetStateAction<boolean>>;
setIsVerifying: React.Dispatch<React.SetStateAction<boolean>>;
setIsVerified: React.Dispatch<React.SetStateAction<boolean>>;
startVerificationTimer: () => void;
startCooldown: () => void;
cooldown: number;
}
여기서 문제 정리 !
EmailHandlerProps의 'setErrors' 의 타입이 React.Dispatch<React.SetStateAction<ErrorState>>; 인 상태에서
* ErrorState- signup 에 활용되는 데이터의 interface
'데이터 타입이 다름에도 불구하고' 비밀번호 재설정에서 ErrorState라는 회원가입 전용 interface를 그대로 가져다 쓴점..
여기서 또 든 의문점?
(1) username도 ErrorState 인터페이스에 때려넣고 optional 로 두면 안되나?
(2) interface를 두개 따로 둔다고 가정하면, EmailHandlerProps의 setErrors부분 때문에 중복으로 EmailHandlerProps 인터페이스를 두어야 하는가?
(1) 때려넣고 optional로 두었더니,, undefined가 될 수 있는 가능성 발생
( 사용하는 곳에서 { username: string; email: string; } 로 타입을 지정해두고 있었음, 물론 Undefined도 가능하게 처리해두면 되지만, 우선 이런 방법은 돌려막기식 같아서 다른 해결 방안을 탐색해봄)
Types of property 'username' are incompatible.
Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.
(2) 중복으로 둘 필요 없다! 제네릭을 활용해보자
-> 제네릭,, <T>를 일단 emailHandler와 EmailHandlerProps의 setErrors에 지정해두었다
-> 또 문제 발생............🥲 (오류 메시지가 너무 많이 떠서 따로 메모는 못했지만,, emailHandler의 handleEmailButtonClick에 무수히 많은 빨간줄 생성 시작)
-> 다른 해결 방법 시도 : <T extends Record<string, any>> 객체로 제네릭을 제한하자
-> 이번엔 no-explicit-any 라는 ,, ESLink 규칙에 걸려버렸다.. (사실 any로 지정할 것이라면 타입스크립트를 사용할 이유가 없기 때문에 매우 납득 가능한 규칙)
(3) 최종 해결 방법 , 인터페이스 구조 수정
정말 도저히 모르겠어서,, GPT 선생님에게 질문한 결과,, 현명한 답변을 주셨다..
확장 가능한 공통 타입인 email 속성으로 CommonErrorState를 만들고, 나머지는 각 인터페이스에 맞게 활용 가능
// 공통 필드 구조 정의
export interface CommonErrorState {
email: string;
[key: string]: string | undefined; // 추가 필드를 허용
}
// 회원가입 시 에러 상태
export interface ErrorState extends CommonErrorState {
name: string;
nickname: string;
id: string;
password: string;
passwordConfirm: string;
phone_number: string;
}
// 비밀번호 재설정 시 에러 상태
export interface ErrorStateChangePW extends CommonErrorState {
username: string;
}
일단 이렇게 마무리 후, 호출 부분에서도 각 인터페이스를 활용하였습니다.. 사실 진짜 타입으로 인한 디버깅은 여러번 경험해왔지만 그동안 제대로 기록을 안한 덕분인지 결국 또 모르게 되었던 것 같습니다.. 오늘도 깨닫는 트러블 슈팅 기록의 중요성,,!
인터페이스에서 공통요소가 있다면 공통 필드 구조를 정의하고 확장 시키는 방법으로 사용하여 다른 인터페이스를 추가하더라도 확장성 있게 사용하는 방법이 중요하다는 것을 느꼈습니다..! 그리고 타입이라는 것, 굉장히 중요하군요,,, 이 디버깅 이슈들 말고도 event에 타입을 지정을 하지 않거나 더 있었지만,,그건 다음에 살펴보도록 하겠습니다 ㅎㅎ 이만 안뇽,,!
'궁금증 해결소' 카테고리의 다른 글
| [라이브러리 개발 일기] Stylelint 가 어떻게 하드코딩을 막을까? (0) | 2026.03.21 |
|---|---|
| [라이브러리 개발 일기] 디자인 시스템 컨벤션 좀 지켜 !! - 1 (1) | 2026.01.06 |
| ‘깨져도 괜찮은 웹’을 만드는 법 – 우아한 낮춤 (0) | 2025.10.20 |
| 프론트 개발자가 바라보는 멱등성🔥 (0) | 2025.10.17 |
| [자바스크립트] 정적 메서드, 인스턴스 메서드 뭐가 달라? (3) | 2024.11.30 |