일단 우리 팀 프로젝트의 결과물을 보여주자면
이런 식으로 다양한 명화가 보여지며 구글 / 깃허브 / 자체 회원가입 세가지 방식으로 로그인이 가능한 방식이다.
해당 컴포넌트를 일일히 뜯어보자!!
import { useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { signinHandler } from '../../api/Supabase.api';
import * as St from './Form.styled';
import { GithubLoginBtn } from './github/Github';
import { GoogleLoginBtn } from './google/Google';
import ImageSlideshow from './slideimage/SlideImage';
일단 위의 것들을 import하여 컴포넌트를 구성하였는데,
useRef란?
React Hooks 중 하나로, 함수형 컴포넌트에서 mutable한 변수를 사용할 수 있게 해줌
이를 통해 DOM 요소에 접근하거나 이전 상태값을 저장하는 등의 작업을 할 수 있다.
일반적으로 React에선 상태(state)를 관리하여 컴포넌트를 업데이트한다.
그러나 때로는 상태로 관리하지 않고, 그냥 값을 저장하거나 읽기만 하길 원할때 바로 useRef를 사용한다 생각하면 된다.
값을 변경할 수 있는 객체
import React, {useRef} from 'react';
function ClickCounter() {
//useRef를 사용하여 변수를 선언
const clickCount = useRef(0);
const handleClick = () => {
// 클릭할 때마다 clickCount.current 값을 증가
clickCount.current += 1;
console.log(`클릭 횟수: ${clickCount.current}`);
};
return (
<div>
<buttun onClick={handleClick}>클릭</button>
</div>
);
}
export default ClickCounter;
여기서 useRef(0)으로 초기화된 'clickCount'는 객체 형태로 반환.
이 객체의 current프로퍼티는 현재 값에 접근할 때 사용.
useRef는 컴포넌트가 리렌더링되어도 값이 유지됨!!! 이 핵심이라 볼 수 있다.
useRef를 사용하면 함수형 컴포넌트에서도 상태를 관리하거나 값을 저장하는 작업이 가능해진다
일단은 이 정도로 알고 넘어가자
상태를 사용할 수 있게 하는 React 훅
React Router에서 제공하는 훅으로, 프로그래밍적으로 페이지 이동을 할 때 사용
supabase와 연동되어 사용할 수 있는 API함수
styled-component로 UI를 꾸미기 위해 팀적으로 Form.styled파일에서 export한 것들을 St이름으로 가져옴
깃헙과 구글 로그인을 위한 버튼 컴포넌트
이미지 슬라이드쇼를 담당
const Form = () => {
const emailRef = useRef<HTMLInputElement | null>(null);
const passwordRef = useRef<HTMLInputElement | null>(null);
const navigate = useNavigate();
const [emailError, setEmailError] = useState('');
const [passwordError, setPasswordError] = useState('');
// 이메일 유효성 검사 함수
const validateEmail = (email: string): boolean => {
if (email.trim() === '') {
setEmailError('');
return false;
}
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!regex.test(email)) {
setEmailError('유효하지 않은 이메일 형식입니다');
return false;
}
setEmailError('');
return true;
};
if (email.trim() === '') {
setEmailError('');
return false;
}
trim() 메서드를 활용하여 양쪽의 공백 제거 후 비어 있는지 확인. 이메일이 비어 있다면 에러 메시지를 초기화하고 false 반환
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!regex.test(email)) {
setEmailError('유효하지 않은 이메일 형식입니다');
return false;
}
정규식을 사용하여 이메일 주소의 형식 검사
^
:문자열의 시작
[^\s@]+
:공백이나 @ 기호가 아닌 문자가 한 번 이상 반복되는 부분을 나타냄. 즉 이메일 주소에서 @ 이전의 문자열
@
:이메일 주소에서 @기호
[^\s@]+
:공백이나 @ 기호가 아닌 문자가 한 번 이상 반복되는 부분을 나타냄. 즉 이메일 주소에서 @ 이후의 도메인
.
:이메일 주소에서 도메인과 최상위 도메인(TLD) 사이의 점
[^\s@]+
:공백이나 @ 기호가 아닌 문자가 한 번 이상 반복되는 부분을 나타냄. 즉 이메일 주소에서 최상위 도메인(TLD)
$
:문자열의 끝을 나타냄
즉, 이메일 주소가 이 형식에 맞지 않으면, 에러 메시지를 설정하고
```
setEmailError('유효하지 않은 이메일 형식입니다');
return false;
```
false를 반환합니다
Top-Level Domain의 약자로, 최상위 도메인을 가리킵니다.
인터넷 주소 체계에서 도메인 이름의 가장 오른쪽 부분을 말합니다
1. 일반 최상위 도메인(gTLD)
가장 일반적으로 사용되는 TLD로, 다양한 종류의 웹시아트에 사용 ex) .com .net .org
2. 국가 코드 최상위 도메인(ccTLD)
특정 국가 또는 지역을 나타내는 도메인으로, 국가 코드(Two-letter country code)로 구성. ex) .uk <-영국 .jp <-일본
3. 고유 최상위 도메인(sTLD 또는 sLD)
특정 목적이나 관심사를 가진 단체나 그룹을 위해 설정된 TLD. ex) .gov(정부기관) .edu(교육기관) .mil(군사기관)
setEmailError('');
return true;
위의 조건을 통과하면 이메일 주소가 유효하다는 것이므로, 에러 메세지 ''로 초기화하고 true로 반환
// 비밀번호 유효성 검사 함수
const validatePassword = (password: string) => {
const regex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[!@#$%^&*()_+{}:<>?])[A-Za-z\d!@#$%^&*()_+{}:<>?]{8,}$/;
if (password === '') {
setPasswordError('');
} else if (!regex.test(password)) {
setPasswordError(
'비밀번호는 최소 8자 이상이며, 최소 하나의 문자와 하나의 숫자, 하나의 특수문자를 포함해야 합니다',
);
} else {
setPasswordError(''); // 성공 메시지
}
};
비밀번호 유효성 검사도 동일하게 적용
but
(?=.[A-Za-z])
:적어도 하나의 알파벳 문자가 포함
(?=.\d)
:적어도 하나의 숫자가 포함
(?=.[!@#$%^&()+{}:<>?])
:적어도 하나의 특수문자가 포함
[A-Za-z\d!@#$%^&*()+{}:<>?]{8,}
:알파벳,숫자,특수문자 중에서 선택된 문자들이 최소 8자 이상
이 부분의 정규식이 있기 때문에 비밀번호에 규제를 걸 수 있습니다!!
const moveToSignUpHandler = () => {
navigate('/register');
};
회원가입 페이지로 이동하기 위한 함수
const handleKeyPress = (event: React.KeyboardEvent) => {
if (event.key === 'Enter') {
signInWithEmail();
}
};
엔터키를 누르면 로그인을 수행하는 함수를 호출하는 이벤트 핸들러
signInWithEmail() <-로그인 수행하는 함수 호출하는 이벤트 핸들러
//사용자 가입
const signInWithEmail = async () => {
try {
const result = await signinHandler(emailRef.current?.value || '', passwordRef.current?.value || '');
if (result.error) {
console.error('로그인중 오류 발생', result.error);
} else {
// 로그인 성공 후 추가 작업 수행
navigate('/');
}
} catch (error) {
console.error('로그인중 오류 발생', error);
}
};
async 즉 비동기 함수로 이메일과 비밀번호를 사용하여 사용자 가입을 시도하는 함수
비동기 작업 수행 중 error가 뜨면 catch블록에서 처리됨
signinHandler 함수를 호출해서 이메일과 비밀번호를 전달
.current 속성을 사용하여 현재 ref가 참조하는 DOM요소에 접근함
값이 없을 경우를 대비하여 '?.'를 사용하여 옵셔널 체이닝을 수행함. 값이 없다면 빈 문자열''로 대체함
옵셔널 체이닝이란?
프로그래밍 언어에서 사용되는 개념으로, 객체의 속성에 안전하게 접근하기 위한 기술.
이 기술은 해당 객체의 속성이나 메서드가 존재하지 않을 때 발생하는 오류를 방지하고 코드를 보다 안전하게 작성할 수 있도록 도와줌
const user = {
name: 'John',
address: {
city: 'New York',
zipcode: '10001'
}
};
만약 'user' 객체에서 'address'객체의 'zipcode' 속성을 안전하게 접근하려면, 일반적으로 다음과 같은 코드를 작성할 수 있음.
if (user && user.address && user.address.zipcode) {
console.log(user.address.zipcode);
} else {
console.log('Zipcode not found');
}
이렇게 작성해도 무방하나 코드가 너무 길고 가독성이 낮음.
이때 옵셔널 체이닝으로 간결히 표현 가능
console.log(user?.address?.zipcode ?? 'Zipcode not found');
여기서 ?.가 옵셔널 체이닝 연산자로, 해당 속성이 존재하지 않을 때 에러를 방지하고 'undefined'를 반환함
??는 nullish 병합 연산자로, 왼쪽 피연산자가 'null'또는 'undefined'일 때 오른쪽 피연산자를 반환함
결과적으로 signinHandler 함수의 결과에서 에러가 생기면, 콘솔에 오류메세지를 보여주고, 성공적 가입이 되었다면 navigate를 통해 메인페이지로 이동함(라우팅 수행)
<St.Container>
<St.FormAndImageSlideWrapper>
<ImageSlideshow />
<St.LoginContainer>
<div>
<St.Title>로그인</St.Title>
<St.InputGroup>
<St.Label>이메일</St.Label>
<St.Input
placeholder="이메일"
type="email"
ref={emailRef}
onChange={() => validateEmail(emailRef.current?.value || '')}
onKeyDown={handleKeyPress}
/>
{emailError && <St.ErrorText>{emailError}</St.ErrorText>}
</St.InputGroup>
<br />
<St.InputGroup>
<St.Label>비밀번호</St.Label>
<St.Input
placeholder="비밀번호"
type="password"
ref={passwordRef}
onChange={() => validatePassword(passwordRef.current?.value || '')}
onKeyDown={handleKeyPress}
/>
{passwordError && <St.ErrorText>{passwordError}</St.ErrorText>}
</St.InputGroup>
<St.Button onClick={signInWithEmail}>로그인</St.Button>
</div>
<br />
<St.SignUpLink onClick={moveToSignUpHandler}>회원이 아니신가요?</St.SignUpLink>
<br />
<St.SocialLoginBtnBox>
<GoogleLoginBtn />
<GithubLoginBtn />
</St.SocialLoginBtnBox>
</St.LoginContainer>
</St.FormAndImageSlideWrapper>
</St.Container>
이렇게 return쪽에 실제 화면에 출력되는 코드는
styled-components를 통해 CSS를 구현하였다.
저기서 ImageSlideshow를 통해 화면에 명화를 몇초간격으로 보여주게 구현하였는데(맨 상단 참고)
import { useEffect, useState } from 'react';
import loginImage1 from '../../../assets/loginimage/loginimage1.webp';
import loginImage2 from '../../../assets/loginimage/loginimage2.webp';
import loginImage3 from '../../../assets/loginimage/loginimage3.webp';
import loginImage4 from '../../../assets/loginimage/loginimage4.webp';
import * as St from './SlideImage.styled';
const images = [
{ image: loginImage1, alt: 'loginimage1' },
{ image: loginImage2, alt: 'loginimage2' },
{ image: loginImage3, alt: 'loginimage3' },
{ image: loginImage4, alt: 'loginimage4' },
];
const ImageSlideshow = () => {
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const handleNextClick = () => {
setCurrentImageIndex((prevIndex) => (prevIndex < images.length - 1 ? prevIndex + 1 : 0));
};
useEffect(() => {
const interval = setInterval(handleNextClick, 5000);
return () => clearInterval(interval);
}, []);
return (
<St.SlideshowContainer>
{images.map((image, index) => (
<St.SlideshowImage
key={index}
src={image.image}
className={index === currentImageIndex ? 'active' : ''}
alt={image.alt}
/>
))}
</St.SlideshowContainer>
);
};
export default ImageSlideshow;
이게 전체 코드이며, 중요한 몇개만 집어보자면
const images = [
{ image: loginImage1, alt: 'loginimage1' },
{ image: loginImage2, alt: 'loginimage2' },
{ image: loginImage3, alt: 'loginimage3' },
{ image: loginImage4, alt: 'loginimage4' },
];
총 4장의 이미지면 일단 images 상수 안에 객체 형태로 담아두고(그 와중에 webp를 사용하여 용량 간소화!!)
const [currentImageIndex, setCurrentImageIndex] = useState(0);
useState 훅을 사용하여 현재 보여지고 있는 이미지의 인덱스를 상태관리 시킨다
const handleNextClick = () => {
setCurrentImageIndex((prevIndex) => (prevIndex < images.length - 1 ? prevIndex + 1 : 0));
};
다음 이미지로 이동하는 핸들러 함수.
현재 이미지의 인덱스를 증가시키고, 마지막 이미지일 경우 0으로 인덱스를 초기화 시킴
useEffect(() => {
const interval = setInterval(handleNextClick, 5000);
return () => clearInterval(interval);
}, []);
useEffect훅을 사용하여 컴포넌트가 마운트될 때 자동 슬라이딩을 시작하도록 설정.
5초(5000)마다 handleNextClick 함수를 호출하여 다음 이미지로 전환.
clearInterval을 사용하여 컴포넌트가 언마운트될 때 인터벌을 정리 - 메모리 누수 방지
setInterval 함수로 handleNextClick함수를 5초마다 호출하는 인터벌 설정
이런 식으로 구현하면 로그인이 뚝딱!!!...
회원가입도 동일한 부분이 많고 거의 다 유효성 검사에 대한 부분이기 때문에 패스!!