귀여운 눈높이에서 작성된, 🐰

하루 한 걸음씩, 성장 하는 중 입니다 🫶🏻

카테고리 없음

Main Project - 회원가입 페이지 UI 구현하기

creamymood 2025. 6. 21. 22:14

예쁜거 보고 힘내기 ㅠ0ㅠ

 

프로젝트 하고 타입스크립트도 본격적으로 쓰고 하니까

함부로 손도 대면 안될 것 같고 (충돌 나고 할까봐...) 정말 정말 조심히 코드 치려고 하고 있다 ㅠ0ㅠ......

 

타입 정의는 언제 해줘야 하며, 어느걸 해줘야하는지 아직 감도 안잡히는데..

플젝에서 쓰려고 하니 나 최큼 힘들어 ... ⭐️


구현 해야 할 UI ↓


 

코드 치기 전, 내가 궁금했던 건

1. 타입 지정은 언제 어디서부터 어떻게 ? 그리고 공통 컴포넌트의 타입은 또 언제?

2. 그냥 js로 먼저 작성한 뒤, 타입 지정 해주면 안되는건가 ?

3. 예를 들어, 회원 가입 페이지를 만들 때 어떤 흐름으로 만드는걸까? 공통 컴포넌트 먼저 제작?

 


3번 흐름 먼저 잡고 가보자. 지피티 사마의 답변이다.

 

타입스크립트 + 리액트 처음 할 때 "순서" 헷갈리는 건 정말 당연해!
보통 회원가입 페이지 작업 순서 (실제 개발자들도 이렇게 해!) !!

 

  1. 대략적인 레이아웃부터 만든다
    → 전체 구조부터 먼저 만들어
    → 페이지에 어떤 요소가 들어가는지, 어떤 게 반복될지 눈으로 확인하는 단계
  2. 중복되거나 재사용될 UI 요소를 발견하면
    → 그때 공통 컴포넌트로 분리
    → 이 시점에서 props랑 타입도 정의
  3. 상세 기능 붙이기
    → 입력값 검증, API 연동, 에러 처리 등 기능 추가

예를 들어 이런식으로 개발자들은 개발한다.

1단계: 초안 구성 (레이아웃)

const SignUpPage = () => {
  return (
    <div>
      <h1>회원가입</h1>
      <input placeholder="이메일" />
      <input placeholder="비밀번호" />
      <button>회원가입</button>

      {/* 소셜 로그인 */}
      <button>카카오 로그인</button>
      <button>네이버 로그인</button>
    </div>
  );
};

 

💡 근데, 이 때는 타입 지정을 안해줘도 될까? 싶었다.

지피티 사마의 답변 ↓

더보기

정확히 거기서 제일 많이 헷갈려 하는 부분이야 —
"초안 만들 때부터 타입을 써야 하나?" 라는 고민.

 

1단계: 대략적인 레이아웃을 만들 때는 꼭 타입 정의 안 해도 돼!

 이 시점엔 타입 없이 JS처럼 자유롭게 UI 먼저 구성해도 좋아.

 

왜냐하면?

  1. UI 구조를 빠르게 확인하는 게 더 중요해서
    → 레이아웃이 어떻게 생겼는지, 어떤 컴포넌트가 필요한지 판단이 먼저야.
  2. 컴포넌트를 분리할지 말지도 아직 모르기 때문에
    → 타입은 컴포넌트를 만들 때 또는 props를 넘겨줄 때 정의하면 충분해!

예시 (초안 단계 - 타입 안 씀)

const SignUpPage = () => {
  return (
    <div>
      <h1>회원가입</h1>
      <input placeholder="이메일" />
      <input placeholder="비밀번호" />
      <button>회원가입</button>

      {/* 소셜 로그인 버튼들 */}
      <button>카카오 로그인</button>
      <button>네이버 로그인</button>
    </div>
  );
};

타입 하나도 없어도 전혀 문제 없지?
이렇게 UI 구조만 먼저 확인해보는 거야.

 

이후: "공통 컴포넌트로 뺄까?" 생각이 들면 이때부터 타입을 작성

type SocialLoginButtonProps = {
  provider: string;
  onClick: () => void;
};

 

💡 방금 설명에서.. 이후: 공통 컴포넌트에서는 타입을 지정해줘야 한다 !? 무슨말이지..

지피티 사마의 답변 ↓

더보기

 

"공통 컴포넌트로 뺄 때는 왜 타입 지정을 꼭 해야 해?"

 

공통 컴포넌트는 “props(속성)”를 받아서 동작하기 때문에, 그 props의 구조를 정확히 알려줘야 해.
그걸 알려주는 게 바로 **타입(Type)**이야.

 

비유로 쉽게 설명해볼게!

Jane이 직접 만든 소셜 로그인 버튼이 있다고 해보자.

<SocialLoginButton provider="kakao" onClick={handleKakao} />

지금 이 SocialLoginButton 컴포넌트는 외부에서 값을 받아야 동작해.
그 "받는 값"이 바로 props고,
props가 어떤 모양인지 설명하는 설명서가 바로 **타입(Type)**이야.

 

비유: 택배 상자 받는 컴포넌트

type SocialLoginProps = {
  provider: string;
  onClick: () => void;
};
  • provider: string (ex. 'kakao', 'naver')
  • onClick: 클릭했을 때 실행할 함수

→ 이렇게 정의해줘야, 나중에 누가 실수로 숫자를 넘기거나 이상한 걸 넘기면
타입스크립트가 바로 에러로 알려줘!

 

예를 들어, 타입 없이 실수하면?

 

<SocialLoginButton provider={false} /> // ❌ boolean 넘겼는데 에러 안남 (JS일 경우)

<SocialLoginButton /> // ❌ 필수 값 빠졌는데도 동작함 (근데 화면 깨짐)

이런 걸 미리 막아주는 역할이 바로 타입이야.

 

이런 식으로 이상한 값이 들어와도 JS에서는 바로 에러가 안 나.
타입스크립트는 이런 실수를 막아주기 위해서 props에 타입을 지정해줘야 해.

 

한 줄 요약

공통 컴포넌트는 "남이 쓸 수 있는 함수"처럼 동작하니까, 정확한 설명서(= 타입)가 꼭 필요하다!

 

다른 건 나중에 해도 되지만

공통 컴포넌트는 타입 지정이 거의 필수!

2단계: 반복되는 요소 분리

"어? 이 로그인 버튼, 로그인 페이지에서도 쓰이잖아!"
공통 컴포넌트로 분리

type SocialLoginProps = {
  provider: 'kakao' | 'naver';
  onClick: () => void;
};

const SocialLoginButton = ({ provider, onClick }: SocialLoginProps) => {
  return (
    <button onClick={onClick}>
      <img src={`/icons/${provider}.svg`} />
      {provider === 'kakao' ? '카카오 로그인' : '네이버 로그인'}
    </button>
  );
};

그리고 이렇게 씀:

<SocialLoginButton provider="kakao" onClick={handleKakao} />
<SocialLoginButton provider="naver" onClick={handleNaver} />

 

3단계: 기능 추가

  • form 입력값 상태 관리
  • 회원가입 API 연결
  • 에러 처리 등

 

💡 한마디 요약💡

레이아웃부터 만들고 → 중복 발견되면 공통 컴포넌트로 분리 → 그때 타입도 같이 작성

 

절대 처음부터 공통 컴포넌트 다 만들려고 하지 않아도 된다.
"반복되는 것 같다?" → 그때 분리
이게 효율적이고 자연스러운 흐름

 


그리고 Join을 만들다보니.. 내가 만든 페이지의 상황을 체크 해보고 싶은데

라우터를 섣불리 등록했다가 충돌 생길 것 같고

우선 페이지라도 잘 작동되는지 app.tsx에 넣어서 실험해보고 싶은데

그러다가 커밋 할 때 문제생길까봐

(나는 깃이란게 내가 작성하는 모~~든 흐름을 다 추적하는 줄 알았는데.)

만만의 오산!

 

깃의 커밋은.. 그러니까, 내가 최종적으로 

그럼 결국 맨 마지막 코드 시점에 add 할 때 그 코드만 저장 되는 것!

 

 

그말은 결국 내가 뭔 난리를 다 쳐도..

뭐 지웠다가 저장했다가 하더라도, 그게 커밋에 들어가는게 아니라

맨 마지막 커밋 시점에 그 때의 최종 코드가 딱 커밋! 되는 거임

 

새로 알게된 사실이

 

실험하는 동안 app.tsx에 Join / 페이지를 넣으니까

바뀌는 부분에 초록색으로 깃이 감지를 했고 git status로 상황을 보니까, app. tsx가 수정되었다고 떴음

 

근데 실험 하고 난 뒤, app.tsx에서 Join을 없애니까 초록색 부분도 사라졌고 git status로 상황을 보니까 그 내용도 지워져있었음

 

그러니까 아무튼 결론!

내가 뭔 난리를 치든 ! 아무튼 커밋은 커밋의 시점은 커밋 시점의 최종 코드!!

 

마치 블로그 글 적을 때 임시저장이 커밋같은 것 같음..

맨 마지막에 내가 적고 임시저장 누르면 그 때 글이 저장~

임시 저장 누르기 전에 내가 뭘 막 지우고 썼다가 지워도 그런 기록들이 저장되진 않고, 최종 임시저장 ! 누르는 순간의 글만 저장되는..

(물론 자동 저장 기능 없단 전제하에)

 


그에따라, 우선 회원가입 Join 페이지의 대략적인 레이아웃은 완성

 

레이아웃을 구성하다보니 또 여러가지 궁금증...

카카오톡 저 박스는 div로 해야할까 버튼으로 해야할까?↓

더보기

좋은 질문입니다! 실무에서는 아래 기준에 따라 button으로 쓸지, div로 쓸지를 명확하게 나눕니다.
정답은 "기능에 따라 다르게 처리한다" 입니다.

실무 기준: button vs div 사용 기준

용도 태그 설명
실제로 클릭해서 로그인/이동/제출 등의 동작이 있는 경우 button 또는 a "사용자 액션"이 있는 요소는 반드시 버튼 계열로 처리해야 함
단순한 스타일 박스, 레이아웃용 정렬 div 클릭 불가능한 구조용 박스는 div로 처리

 

카카오 로그인 버튼은?

// ✅ 실무에서는 무조건 <button> 사용!
<button onClick={handleKakaoLogin}>카카오로 3초만에 가입하기</button>
  • 이유: 카카오 로그인은 사용자 액션을 트리거하는 인터랙티브 요소니까 div로 하면 접근성(A11y)시멘틱 구조에 어긋나요.
  • 보조기기(Screen Reader)에서도 button은 "클릭 가능한 요소"로 인식되고, div는 인식 못 합니다.

 

실수 예시 (안 좋은 사례)

// ❌ 클릭 동작이 있는 걸 div로 처리하면 안 됨
<div onClick={handleKakaoLogin}>카카오 로그인</div>

 문제점

  • 시멘틱 웹 규칙 위반
  • 접근성 문제
  • 키보드 접근 (Tab, Enter) 안 됨
  • 유지보수 시 혼동 가능

 

 팁: 시멘틱 + 접근성을 고려한 버튼 작성법

  • 로그인, 회원가입, 제출 등 사용자 동작 트리거반드시 <button>
  • 스타일만 입히고 싶을 땐 버튼에 style, className만 쓰면 됨 (굳이 div 필요 없음)
  • 아이콘/이미지도 모두 버튼 내부에 넣으면 OK (스타일만 잘 정리)

 정리하자면


로그인 버튼, 소셜 버튼 ✅ button
페이지 이동 링크 ✅ a
단순 스타일 박스 div

"사용자가 클릭할 수 있는 기능이 있다"면 무조건 button이나 a 태그를 써야 합니다.

 


 

이렇게 코드를 치다보니,, 

얼떨 결에 ui도 완성은 됐는데, (아직 라우터나 이런건 등록 전)

 

카카오톡이랑 네이버 부분이 공통된 부분이 많아서, 조금 코드도 길어지고 복잡해져서.

공통 버튼 컴포넌트로 분리해보려고 함

import React from "react";

function Join() {


    return(
        <div className="flex flex-col items-center pt-20">
            <img src="src/assets/logo_black.png" alt="오즈코딩스쿨 로고" className="w-[180px] mb-4"/>
            <div className="flex gap-3 text-[16px]">
                <p className="text-[#4D4D4D]"> 회원 이신가요?</p>
                <p className="text-[#6201E0]"> 로그인 하기</p>
            </div>
            <br/>
            <br/>

             {/* 카카오톡 회원 가입 버튼  */}
            <button 
                style={{backgroundColor: "#FEE500", 
                            width: "348px", 
                            height: "52px", 
                            borderRadius: "4px", 
                            color: "#391C1A",
                            padding: "8px",
                            marginBottom: "12px",
                            display: "flex",                // ✅ 필수!
                            alignItems: "center",          // ✅ 수직 정렬
                            justifyContent: "center",      // ✅ 가운데 정렬
                            gap: "10px",                   // ✅ 간격 조정
                            }}>
                <img src="src/assets/kakao_brown.svg" alt="카카오" />

                카카오로 3초만에 가입하기</button>

                 {/* 네이버 회원 가입 버튼  */}
            <button 
                style={{backgroundColor: "#03C75A", 
                            width: "348px", 
                            height: "52px", 
                            borderRadius: "4px", 
                            color: "#FFFFFF",
                            padding: "8px",
                            marginBottom: "25px",
                            display: "flex",                // ✅ 필수!
                            alignItems: "center",          // ✅ 수직 정렬
                            justifyContent: "center",      // ✅ 가운데 정렬
                            gap: "10px",                   // ✅ 간격 조정
                            }}>
                <img src="src/assets/naver_white.svg" alt="네이버" />

                네이버로 가입하기 </button>

            <p className="text-[#4D4D4D] underline"> 일반회원 가입</p>

        </div>

    )
}

export default Join

 

그 때, 팀원분 Exellent님께서 이럴 때 버튼은 공통으로 빼는게 좋고

그 안에서 공통되는 디자인 부분은 상수로 또 빼는 것도 알려주셔서 공부 겸 짚고 넘어가기 .!

 

스타일 상수와, 공통 컴포넌트 차이 ?

더보기

좋은 질문이에요! 스타일 상수와 공통 컴포넌트가 어떤 차이가 있는지, 좀 더 쉽게 설명해볼게요.


1. 스타일 상수 (Style Constants)

  • 말 그대로 '스타일 값'만 변수로 빼놓은 것이에요.
  • 예를 들어, 버튼 배경색, 글자색, 폰트 크기, 여백 같은 걸 변수로 미리 정해놓고, 여러 군데서 같은 값을 쓸 때 재사용하는 거예요.

예시

// styleConstants.js
export const COLORS = {
  kakaoYellow: "#FEE500",
  naverGreen: "#03C75A",
  textDark: "#391C1A",
};

export const FONT_SIZES = {
  button: "16px",
};

// 버튼 스타일에 변수 쓰기
const kakaoButtonStyle = {
  backgroundColor: COLORS.kakaoYellow,
  color: COLORS.textDark,
  fontSize: FONT_SIZES.button,
  padding: "12px 24px",
  borderRadius: "4px",
};
  • 위처럼 색상이나 글꼴 크기, 간격 같은 걸 변수로 빼놓고 버튼 스타일에 사용해요.
  • 이렇게 하면 스타일 변경할 때 변수 값만 바꾸면 되니 편해요.

2. 공통 컴포넌트 (Common Component)

  • UI 요소 전체를 재사용할 수 있게 컴포넌트로 만드는 것이에요.
  • 버튼 모양, 텍스트, 클릭 동작 등 완성된 UI 단위를 하나로 묶는 거예요.

예시 (리액트 컴포넌트)

import React from "react";
import { COLORS, FONT_SIZES } from "./styleConstants";

function SocialButton({ provider, onClick }) {
  const styles = {
    kakao: {
      backgroundColor: COLORS.kakaoYellow,
      color: COLORS.textDark,
    },
    naver: {
      backgroundColor: COLORS.naverGreen,
      color: "#fff",
    },
  };

  return (
    <button
      style={{
        ...styles[provider],
        fontSize: FONT_SIZES.button,
        padding: "12px 24px",
        borderRadius: "4px",
        border: "none",
        cursor: "pointer",
      }}
      onClick={onClick}
    >
      {provider === "kakao" ? "카카오 로그인하기" : "네이버 로그인하기"}
    </button>
  );
}

export default SocialButton;
  • 이렇게 버튼을 컴포넌트로 만들면
    • 버튼 모양, 크기, 동작(클릭 이벤트 등)을 한 군데서 관리 가능
    • 나중에 버튼을 추가하거나 디자인을 바꿀 때 아주 편해요
    • 버튼을 여러 곳에서 동일하게 사용할 수 있어요

차이점 정리

구분 스타일 상수 공통 컴포넌트
역할 색상, 폰트 크기, 간격 등 스타일 값 저장 UI 요소(버튼, 카드 등) 전체를 재사용 가능하게 만듦
대상 디자인 속성 (숫자, 문자열) UI 기능과 구조가 포함된 컴포넌트 코드
예시 const primaryColor = "#6201E0"; <SocialButton provider="kakao" />
장점 스타일 변경 시 유지보수 용이 UI 일관성 유지, 코드 재사용성 증가
보통 같이 쓰임? 네, 스타일 상수는 공통 컴포넌트 안에서 자주 사용됨 공통 컴포넌트 안에 스타일 상수를 활용

필요하면 스타일 상수만 먼저 만들고, 그걸 활용해서 공통 컴포넌트를 만드는 식으로도 많이 해요.

더 궁금한 점 있으면 편하게 물어봐 주세요!

그런데

결국 어찌 됐든.. 스타일 상수만 썼을 때는, 여전히 코드가 복잡해질 것 같아서

우선 공통 컴포넌트로 빼보겠습니다 

 

아 그리고, 상수는 같은 파일내만 쓸 수 있어서,,

상수는 공통 컴포넌트로 안뺄때 반복 안될때 한 파일에서 쓸 때 유용 하겠다 ~

더보기

맞아요!

상수로 스타일만 빼면 색깔, 폰트 크기 같은 값은 재사용되지만,
버튼을 만드는 코드(구조, 텍스트, 이벤트 등)는 여전히 중복될 수 있거든요.

그래서 버튼 모양이 비슷하고 역할도 같으면
컴포넌트로 쭉 빼서 재사용하는 게 코드도 훨씬 깔끔해지고 관리하기 편해요.

스타일 상수랑 공통 컴포넌트 둘 다 잘 활용하면
유지보수성과 생산성이 확실히 올라가니까 좋은 습관이에요!

필요하면 내가 간단한 컴포넌트 예시도 바로 만들어 줄게요~

 

공통 컴포넌트로 만들 때 타입 지정에 대해 또 고민이었다.

어디서 부터 어디까지 타입을 지정해야 하나..

 

더보기

1. 공통된 부분 (버튼 공통 컴포넌트 내부 고정 값)

  • 버튼 사이즈 (높이, 너비)
  • 기본 테두리, 모서리 둥글기
  • 기본 폰트 스타일 등

→ 이 부분은 컴포넌트 내부에 하드코딩하거나 스타일 시트에 고정으로 두고, 타입 지정 필요 없음!

2. 달라질 부분 (프롭스로 받는 부분)

  • 배경 색상 (bgColor)
  • 내부 글자 (children 또는 label)
  • 내부 로고 (iconSrc 또는 icon)
  • 바닥 마진 (marginBottom)

→ 이 부분만 props로 받고 타입 지정해줘야 함!

 

 

type ButtonProps = {
  bgColor?: string;
  children: React.ReactNode;
  iconSrc?: string;
  marginBottom?: string | number;
  onClick?: () => void;
};

 

 

이렇게 하면 재사용도 쉽고, 고정 부분은 유지하면서 변할 부분만 유연하게 바꿀 수 있어!

 

 

type ButtonProps = {
  bgColor?: string;
  children: React.ReactNode;
  iconSrc?: string;
  marginBottom?: string | number;
  onClick?: () => void;
};

 

여기서, children 부분:

children: React.ReactNode는 React 컴포넌트에서 특별한 타입이다

children 이란?

  • React 컴포넌트 안에 들어가는 중첩된 요소텍스트를 의미해.
  • 예를 들어, <Button>Click me</Button>에서 "Click me"가 바로 children이야.

React.ReactNode란?

  • React에서 렌더링할 수 있는 모든 걸 포함하는 타입이야.
  • 문자열, 숫자, JSX 요소, 배열, null, undefined, false 등 모두 포함돼.

예를 들면:

<Button>Click me</Button>                   // 문자열
<Button><span>Click</span> me</Button>     // JSX 요소 섞인 것
<Button>{condition && <Icon />}</Button>   // 조건부 렌더링도 가능

이 모든 경우를 아우르는 타입이 바로 React.ReactNode야.

정리하자면

  • children: React.ReactNode라고 하면
  • 버튼 안에 텍스트, 아이콘, 여러 JSX 요소 등 어떤 형태든 다 넣을 수 있다는 뜻!

 

그렇다면.. 왜 <SocialButton>내용</SocialButton> 이렇게 썼을까?

children이 있어서!

  • React에서 컴포넌트 태그 사이에 넣은 내용이 바로 children prop으로 전달
  • 그래서 <SocialButton>카카오로 가입하기</SocialButton> 처럼
    태그 사이에 글자, JSX 요소, 아이콘 등 뭐든 넣으면 그게 children이 돼서 컴포넌트 내부에서 렌더링

반면에 <Button /> 처럼 닫히는 태그는?

  • 컴포넌트에 전달할 내용(children)이 없을 때 쓰는 문법.
  • <Button /> 은 children이 없다는 뜻!
  • 만약 children을 기대하는 컴포넌트인데 빈 태그로 닫으면, 내부에 표시할 내용이 없으니까 화면에 아무 것도 안 나올 수 있어.

정리하자면

  • <SocialButton>내용</SocialButton> = children에 내용이 전달됨
  • <SocialButton /> = children이 없어서 내부에 아무 것도 없음

예시

// children이 있는 경우
<SocialButton>로그인</SocialButton>

// children이 없는 경우
<SocialButton />   // 내부에 내용이 없으니까 버튼 안에 아무 것도 안 나옴

 


onclick 부분!

onClick?: () => void; 이건,

  • onClick이라는 이름의 prop이고,
  • 함수 타입이라는 뜻이야.
  • ?는 이 prop이 선택적(optional) 이라는 뜻. 즉, 전달 안 해도 된다는 의미고.

자세히 풀면:

  • () => void 는
    • 매개변수(parameter)는 없고,
    • 반환값(return value)도 없는 함수 타입이야.
  • 즉, 아무 인자도 받지 않고, 아무것도 반환하지 않는 함수를 의미해.

왜 쓰는지?

  • 버튼이 클릭됐을 때 실행할 함수를 부모 컴포넌트가 넘겨줄 수 있게 하기 위해서!
  • 예를 들어:
<Button onClick={() => alert("버튼 클릭됨!")}>클릭</Button>

버튼을 누르면 alert가 실행돼.

정리

  • onClick은 클릭 이벤트 핸들러를 받는 프롭이고,
  • 전달 안 해도 되니까 ? 붙음,
  • 함수는 인자 없이 아무것도 반환하지 않는 타입이야.

 

 

 

그랬을 때 코드는 다음과 같다.

//공통 버튼 컴포넌트 

import React from "react";

type SocialButtonProps = {
    bgColor: string;
    iconSrc: string;
    iconAlt: string;
    children: React.ReactNode
    marginBottom?: string | number;
    onClick?: () => void;
    txtColor: string
}

export default function SocialButton({
    bgColor,
    iconSrc,
    iconAlt,
    children,
    marginBottom,
    onClick,
    txtColor
}: SocialButtonProps) {

    return (
        <button
          onClick={onClick}
          style={{
            backgroundColor: bgColor,
            width: "348px",
            height: "52px",
            borderRadius: "4px",
            padding: "8px",
            color: txtColor,
            marginBottom: marginBottom,
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            gap: "10px",
          }}
        >
          <img src={iconSrc} alt={iconAlt} />
          {children}
        </button>
      );


}

 

//직접 사용하기 코드

: 추후에, 백엔드랑 연결 후, onclick 변경해줘야 함!

더보기

네, 맞아요!

지금은 onClick에 간단히 alert 함수 넣어서 테스트하고 있는데,
나중에 백엔드 API랑 연동할 때는 onClick 안에 실제 소셜 로그인/회원가입 로직을 넣으면 돼요.

예를 들면:

  • 카카오 로그인 API 호출
  • 네이버 로그인 API 호출
  • 로그인 성공 시 받은 토큰 처리
  • 사용자 정보 받아서 상태 관리 (Redux, Context API 등)

아래처럼 onClick 함수만 변경하면 됩니다:

<SocialButton
  bgColor="#FEE500"
  txtColor="#391C1A"
  iconSrc="src/assets/kakao_brown.svg"
  iconAlt="카카오"
  marginBottom="12px"
  onClick={handleKakaoLogin}
>
  카카오로 3초만에 가입하기
</SocialButton>
const handleKakaoLogin = async () => {
  try {
    // 카카오 로그인 API 호출 및 처리
  } catch (error) {
    console.error("카카오 로그인 실패", error);
  }
};

필요하면 로그인 성공 후 리다이렉트나 사용자 상태 업데이트도 같이 해주면 되고요!
지금처럼 UI만 먼저 구현해놓고, 나중에 기능 연결하는 방식이 보통이에요.

import SocialButton from "@/components/common/SocialButton";
import React from "react";

function Join() {


    return(
        <div className="flex flex-col items-center pt-20">
            <img src="src/assets/logo_black.png" alt="오즈코딩스쿨 로고" className="w-[180px] mb-4"/>
            <div className="flex gap-3 text-[16px]">
                <p className="text-[#4D4D4D]"> 회원 이신가요?</p>
                <p className="text-[#6201E0]"> 로그인 하기</p>
            </div>
            <br/>
            <br/>

            {/* 카카오로 회원 가입 버튼  */}
            <SocialButton
                bgColor="#FEE500"
                txtColor="#391C1A"
                iconSrc="src/assets/kakao_brown.svg"
                iconAlt="카카오"
                marginBottom="12px"
                onClick={() => alert("카카오 로그인 클릭됨")}>
                    카카오로 3초만에 가입하기
            </SocialButton>

            {/* 네이버로 회원 가입 버튼  */}
            <SocialButton
                bgColor="#03C75A"
                txtColor="#FFFFFF"
                iconSrc="src/assets/naver_white.svg"
                iconAlt="네이버"
                marginBottom="25px"
                onClick={() => alert("네이버 로그인 클릭됨")}>
                    네이버로 가입하기
            </SocialButton>


            <p className="text-[#4D4D4D] underline"> 일반회원 가입</p>

        </div>

    )
}

export default Join

2. 페이지를 따로 만들어서 (일반 회원가입용) 만들기

다음 게시글에서...