Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ 4주차 기본/심화/생각 과제 ] 🍁 로그인/회원가입 #4

Open
wants to merge 13 commits into
base: main
Choose a base branch
from

Conversation

aazkgh
Copy link
Member

@aazkgh aazkgh commented Nov 16, 2023

✨ 구현 기능 명세

🌱 기본 조건

  • .env 파일 사용하기

🧩 기본 과제

[ 로그인 페이지 ]

  1. 로그인
    • 아이디와 비밀번호 입력후 로그인 버튼을 눌렀을시 성공하면 /mypage/:userId 로 넘어갑니다. (여기서 userId는 로그인 성공시 반환 받은 사용자의 id)
  2. 회원가입 이동
    • 회원가입을 누르면 /signup으로 이동합니다.

[ 회원가입 페이지 ]

  1. 중복체크 버튼

    • ID 중복체크를 하지 않은 경우 검정색입니다.
    • ID 중복체크 결과 중복인 경우 빨간색입니다.
    • ID 중복체크 결과 중복이 아닌 경우 초록색입니다.
  2. 회원가입 버튼

    • 다음의 경우에 비활성화 됩니다.
    • ID, 비밀번호, 닉네임 중 비어있는 input이 있는 경우
    • 중복체크를 하지 않은 경우
    • 중복체크의 결과가 중복인 경우
    • 회원가입 성공시 /login 으로 이동합니다.

[ 마이 페이지 ]

  1. 마이 페이지
    • /mypage/:userId 의 userId를 이용해 회원 정보를 조회합니다.
    • 로그아웃 버튼을 누르면 /login으로 이동합니다.

🌠 심화 과제

[ 로그인 페이지 ]

  1. 토스트
    • createPortal을 이용합니다.
    • 로그인 실패시 response의 message를 동적으로 받아 토스트를 띄웁니다.

[ 회원가입 페이지 ]

  1. 비밀번호 확인

    • 회원가입 버튼 활성화를 위해서는 비밀번호와 비밀번호 확인 일치 조건까지 만족해야 합니다.
  2. 중복체크

    • 중복체크 후 ID 값을 변경하면 중복체크가 되지 않은 상태(색은 검정색)로 돌아갑니다.

생각과제

  • API 통신에 대하여
  • 로딩 / 에러 처리를 하는 방법에는 어떤 것들이 있을까?
  • 패칭 라이브러리란 무엇이고 어떤 것들이 있을까?
  • 패칭 라이브러리를 쓰는 이유는 무엇일까?

💎 PR Point

✔ 처음 host에 접근할 때 로그인 페이지로 리다이렉트

  • 호스트에 접근해서 주소창에 페이지 직접 입력해주는 것보단 flow 상 리다이렉트를 적용시켜주면 좋을 것 같아서 자체 심화과제를 해보았습니다,,,
  • router v6 에서는 <Redirect />를 더이상 지원하지 않기 때문에 다음과 같은 방식으로 구현해 주었습니다.
       <Routes>
          <Route path="/" element={<Navigate replace to="/login" />} />
          <Route path="/login" element={<LogIn />} />
          <Route path="/signup" element={<SignUp />} />
          <Route path="/mypage/:userId" element={<MyPage />} />
       </Routes>

✔ 회원 가입 버튼 활성화

  • useEffect를 활용해서 input 값들이 변화할 때마다 렌더링될 수 있게 구현했습니다.
  • 아이디 중복 상태를 빈 상태, 중복값 존재, 중복값 존재 X 3개로 나누어서 마지막 경우에만 버튼이 활성화될 수 있도록 구현했습니다.
useEffect(() => {
        if ( id && pw && checkPw && nickname 
            && checkPw === pw && exist === false) {
            setBtnState(true);
        }
        else
            setBtnState(false);
      }, [id, pw, checkPw, nickname, exist]
    );

✔ 아이디 input 값에 따라 실시간 변화 감지

  • 아이디 중복 확인을 완료한 다음에 input에 변화가 생기면 재확인 상태로 바꿔주는 함수를 구현했습니다.
  • 다른 input 값들과 달리 회원 가입 버튼을 비활성화 해주고 중복 확인 상태를 빈 값으로 설정해주는 setter 함수를 함수에 포함시켰습니다.
    const idEvent = (event) => {
        setId(event.target.value); 
        setExist(); 
        setBtnState(false);
    };

✔ Toast 구현

  • index.html에서 <div id="root"></div> 태그 밑에 <div id="toast"></div>를 삽입해줘서 여기에 로그인 오류가 발생할 때마다 토스트가 뜰 수 있게 해주었습니다.
  • 오류 발생 -> 토스트 생성 -> 1,2초 뒤에 사라짐이라는 일련의 과정을 반복할 수 있도록 오류 발생 때마다 fail state를 변경해줘서 useEffect가 실행될 수 있게 해주었습니다.
//toast가 일정 시간 뒤에 사라지게
    useEffect(() => {
        let timer = setTimeout(()=>{
            setFail('');
        }, 1200);
        
        return () => {clearTimeout(timer)}
    }, [fail]);

//오류 발생시 toast 생성
      {fail
      ? createPortal(<Toast>{fail}</Toast>, document.getElementById("toast"))
      : null }

🥺 소요 시간, 어려웠던 점

  • 12h
  • 처음에 감을 잡는 것이 힘들었는데 감 잡은 후에는 크게 어려웠던 건 없었습니다! 성장한 것 같아서 기분 좋았어욤ㅎㅎ
  • 서버에서 api 주고받는 경험을 처음 해봤는데 재밌었습니다!

🌈 구현 결과물

default.mp4

@aazkgh aazkgh self-assigned this Nov 16, 2023
@aazkgh aazkgh changed the title [4주차 기본/심화/생각 과제] 로그인/회원가입 [ 4주차 기본/심화/생각 과제 ] 로그인/회원가입 Nov 16, 2023
@aazkgh aazkgh changed the title [ 4주차 기본/심화/생각 과제 ] 로그인/회원가입 [ 4주차 기본/심화/생각 과제 ] 🍁 로그인/회원가입 Nov 17, 2023
Copy link

@eonseok-jeon eonseok-jeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

과제 하시느라 고생하셨습니다!! API 붙이는 게 쉽지 않은 일인데 심화과제까지 다하셨다니 정말 멋진 거 같아요 🥇

return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Navigate replace to="/login" />} />

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

path="/"일 때 /login으로 이동시켜줘도 되지만
그냥 바로 해주셔도 무방해요~
경로를 한 번 더 이동하지 않아서 오히려 더 좋을지도 모르죠?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

바로 해준다는게 그냥 element로 Login을 넣어준다는 얘긴인가요???
그렇다면 Navigate를 안불러와도 되긴 하겠네요-!!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 맞아여!


import axios from 'axios';

const LogIn = () => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React에서 component 파일 이름은 Pascal case를 이용하는 편이에요! 관습이죠
따라서 login.jsx 보다는 Login.jsx로 작성해보시는 걸 추천드려요 :)
물론 본인의 취향대로 작성하셔도 됩니다


const Router = () => {
return (
<BrowserRouter>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BrowserRouter말고 createBrowserRouter라는 것이 있어요!
공식 문서를 보시면 createBrowserRouter를 recommend 하고 있어요 :)

구현하는 방법은 조금 비슷해서 한 번 찾아보시면 충분히 이해가능하실 거예요

참고 자료 : https://reactrouter.com/en/main/routers/create-browser-router

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 createBrowserRouter는 또 첨 알았네 배워가욥!!!


const handleLogin = async () => {
try{
const response = await axios.post(`${import.meta.env.VITE_BASE_URL}/api/v1/members/sign-in`, {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

axios가 제공하는 기능 중 하나인 create를 사용해보시면 매번 요청할 때마다 경로를 다 입력하는 수고로움을 덜 수 있어요
그 외에도 다양한 설정들을 할 수 있어서 이를 한 번 이용해 보시면 좋을 거 같아요

참고 자료 : https://axios-http.com/docs/instance

username: id,
password: pw,
});
response&&navigate(`/mypage/${response.data.id}`);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사소한 건지만 띄어쓰기 같은 거 잘 지켜주시면 가독성이 올라가요 :)

response && navigate

이런 식으로요
물론! 난 띄어쓰기 안 하는게 가독성 더 좋은데? 하시면 그렇게 하시면 됩니다!!!

추가로 indent도 4로 설정이 되어 있으신데, 그럴 경우 코드가 점점 복잡해지고 tag가 점점 중첩될수록 코드를 읽기 불편해져서 indent size를 2로 하시는 분들도 많습니다!! 한 번 고려해 보세요 :)

<Modal>
<Header>My Page</Header>
<InfoContainer>
<Image src='../../public/icon.jpg'></Image>
Copy link

@eonseok-jeon eonseok-jeon Nov 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public에 있는 건 경로 생략이 가능해요!

<Image src='/icon.jpg' />

로 바로 하셔도 됩니다.

그리고 이건 제 취향인데 tag 사이에 어떠한 내용도 안 들어갈 경우 <></> 이런 식으로 하기보단 위와 같이 < /> 이런 식으로 바로 닫아주는 편입니다
저는 이게 더 깔끔한 거 같더라고요? 그냥 저의 개인적인 의견이니 가볍게 넘겨주세요 :)

추가로 img tag에는 alt 속성을 추가해시면 좋아요
이유가 궁금하시면 한 번 찾아보심이,,

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alt 달아주기!!!!!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

내가 할 말 다 적어놨다,,,
img 태그는 주로 셀프 클로징 태그로 많이 사용되니까 참고~~

<Info>닉네임: {name}</Info>
</InfoWrapper>
</InfoContainer>
<Button onClick={()=>navigate(`/login`)}>로그아웃</Button>
Copy link

@eonseok-jeon eonseok-jeon Nov 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 버튼에 특정 event handling이 필요하지 않으므로 그냥 a tag를 이용해서 routing 해줄 수도 있어요!
그럼 useNavigate() 함수를 import 하지 않아도 되죠

하지만 a tag 이용할 경우 해당 버튼 클릭 시 새로고침이 되기 때문에 이를 방지하고자 react-router-dom에는 Link 라는 기능을 제공하고 있어요 :)

구글링해보시면 아주 자세하게 설명이 되어 있으니 찾아서 사용해보셔도 좋을 거 같아요

Comment on lines 19 to 27
useEffect(() => {
if ( id && pw && checkPw && nickname
&& checkPw === pw && exist === false) {
setBtnState(true);
}
else
setBtnState(false);
}, [id, pw, checkPw, nickname, exist]
);
Copy link

@eonseok-jeon eonseok-jeon Nov 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사실 조금 어려운 내용인데요 이해가 되지 않으시면 그냥 아 이런 게 있구나 하고 넘어가시면 될 거 같아요 :)

이 상황에선 useEffect를 사용하지 않아도 돼요!

const [exist, setExist] = useState();

const [id, setId] = useState('');
const [pw, setPw] = useState('');
const [checkPw, setCheckPw] = useState('');
const [nickname, setNickname] = useState('');

const navigate = useNavigate();

//회원 가입 버튼 활성화
if ( id && pw && checkPw && nickname 
     && checkPw === pw && exist === false) {
     setBtnState(true);
}
else
    setBtnState(false);

이런 식으로 바로 작성하셔도 돼요

왜냐면 종속성으로 들어간 id, pw, ... 들은 다 state 잖아요?
그 말은 그 state 값들이 변경이 되면 해당 컴포넌트가 re-rendering 된다는 거예요
re-rendering되면 component 안에 있는 모든 코드들이 다시 실행이 되는 건데
그럼 코드들 중 하나인 위의 if - else문도 다시 실행이 된다는 거죠!
그럼 값이 변경될 때마다 새롭게 if-else문이 실행이 되니, btnState가 매번 새로운 값으로 초기화 되는 거죠?

정리하자면, state 변경 -> re-rendering -> if else 문 재실행 -> 최신 상태의 btnState값 얻게 됨
이런 과정을 거치게 되는 거예요

useEffect를 많이 사용하는 건 성능면에서 안 좋을 수 있어서 사용하지 않을 수 있다면 안 하는 게 좋아요

이와 관련해선 제가 React 스터디에서 useEffect article에 보다 상세하게 적어놨으니 이를 참고하시면 좋을 거 같아요!! useEffect에 대한 공식문서를 먼저 참고하셔도 좋아요 :)

혹시라도 이해가 되지 않거나 궁금한 점 있으면 언제든지 여쭤봐주세요

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우옹 이거 너무 유용한 정보다,, 나두 요 부분 useEffect로 구현했는데
반영해서 리팩토링 해봐여징🏃🏼‍♀️💨

//아이디 이벤트 발생시 작동하는 함수
const idEvent = (event) => {
setId(event.target.value);
setExist();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

빈 값으로 두시지 마시고, 초기값을 할당해주세요
isExist의 type이 뭔지 파악하시고 이에 해당하는 초기값을 설정하시면 될 거 같아요

Copy link
Member Author

@aazkgh aazkgh Dec 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 이렇게 초기값을 할당한 이유가 중복 체크 검사 상태를

  1. 초기에 중보 체크 검사 버튼을 누르지 않은 상태
  2. 중복 결과 존재
  3. 중복 결과 존재 X

이렇게 3개로 나누고 싶었기 때문인데요.

뭔가 2, 3번에 적합한 타입이 boolean 값이라 이렇게 설정했는데 저도 초기값을 설정 안 하는 게 찝찝하긴 하더라고요,,? 그래서 string값으로 줘야 하나 싶긴한데,,

혹시 초기값으로 null 값을 안 주는 데에 이유가 있을까요 ? 느낌적인 느낌으로 그냥 그렇게 하면 안될 거 같긴 한데 명확한 이유가 궁금합니다,,구글링해도 딱 이렇다 ! 하는 이유가 없어서 궁금해용

Comment on lines 67 to 74
onKeyUp={(e) => {idEvent(e)}}/>
<Button className={exist === true ? "exist" : exist === false ? "notExist" : null }
type="button" onClick={idCheck}>중복체크</Button>
</LoginInput>
<LoginInput>
<InputTitle>비밀번호</InputTitle>
<Input type="text" name='password' id='password'
onKeyUp={(e) => setPw(e.target.value)}/>
Copy link

@eonseok-jeon eonseok-jeon Nov 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정말 사소한 부분이지만,

위의 onKeyUp을(67줄) 보시면 { () => {} } 이런 구조로 되어 있고,
아래의 onKeyUp은(74줄) { () => } 이런 구조(중괄호 생략)로 되어 있네요
이런 부분에 있어서 통일성을 갖고 코드를 작성하시면 가독성이 더 좋아질 거 같아요
중괄호를 추가하실 거면 ; 붙이는 것도 잊지마시고요!
중괄호 시작하고 띄어쓰기, 끝나기 전에 띄어쓰기를 붙여 가독성을 좀 더 높일 수 있어요 ( ex: { idEvent(e) }; )

이런 것들 일일이 지키기 귀찮으시면 prettier를 이용해 보세요
구글링을 통해 이에 대해서는 많은 양의 자료를 찾아볼 수 있고, 또 심심스 prettier article에도 잘 정리가 되어 있으니 참고해보시면 좋을 거 같아요

Copy link
Member

@jungwoo3490 jungwoo3490 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

역시 심화까지 깔끔.....👍🏻👍🏻👍🏻 합세도 화이팅이야!!!!! 🔥🔥🔥

@@ -0,0 +1,26 @@
.env
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻👍🏻👍🏻

<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>로그인 및 회원가입</title>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻👍🏻👍🏻👍🏻👍🏻

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="ko">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻👍🏻👍🏻👍🏻👍🏻


const Router = () => {
return (
<BrowserRouter>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 createBrowserRouter는 또 첨 알았네 배워가욥!!!

<InputTitle>ID</InputTitle>
<Input type="text" name="username" id="username"
placeholder="ID를 입력해주세요"
onKeyUp={(e) => setId(e.target.value)}/>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞아 보통 input 값은 onChange 이벤트로 핸들링하는데 onKeyUp 쓴 이유가 궁금해!!

<SignUpBtn onClick={()=>navigate(`/signup`)}>회원 가입</SignUpBtn>
</Modal>
{fail
? createPortal(<Toast>{fail}</Toast>, document.getElementById("toast"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 요롷게 토스트팝업 뜰 수 있게 할 수 있구나 배워가요 ㅎㅅㅎ

</LoginInput>
</InputContainer>
<LogInBtn onClick={handleLogin}>로그인</LogInBtn>
<SignUpBtn onClick={()=>navigate(`/signup`)}>회원 가입</SignUpBtn>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나는 navigate onClick 이벤트 핸들러 함수로 따로 빼줬는데 이렇게 인라인으로 작성하는 게 더 낫겠다....

<Modal>
<Header>My Page</Header>
<InfoContainer>
<Image src='../../public/icon.jpg'></Image>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alt 달아주기!!!!!

import axios from 'axios';

const SignUp = () => {
const [btnState, setBtnState] = useState(false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

개인적인 생각인데 단순 btnState보다는 이 state의 역할을 잘 나타내주는 이름으로 해주는 게 가독성이 더 좋을 것 같다는 느낌!!
이 state는 회원가입 가능 여부를 판단하는 state니까 isEnableSignIn 같은 이름이 더 좋겠다!!

Comment on lines 19 to 27
useEffect(() => {
if ( id && pw && checkPw && nickname
&& checkPw === pw && exist === false) {
setBtnState(true);
}
else
setBtnState(false);
}, [id, pw, checkPw, nickname, exist]
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
useEffect(() => {
if ( id && pw && checkPw && nickname
&& checkPw === pw && exist === false) {
setBtnState(true);
}
else
setBtnState(false);
}, [id, pw, checkPw, nickname, exist]
);
useEffect(() => {
id && pw && checkPw && nickname
&& checkPw === pw && exist === false ? setBtnState(true) : setBtnState(false)
}, [id, pw, checkPw, nickname, exist]
);

요롷게 간단하게 해줄 수 있겠다!!

Copy link

@SynthiaLee SynthiaLee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고해쪄잉 합세도 홧팅!!!💗

return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Navigate replace to="/login" />} />

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

바로 해준다는게 그냥 element로 Login을 넣어준다는 얘긴인가요???
그렇다면 Navigate를 안불러와도 되긴 하겠네요-!!

<InputTitle>ID</InputTitle>
<Input type="text" name="username" id="username"
placeholder="ID를 입력해주세요"
onKeyUp={(e) => setId(e.target.value)}/>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onKeyUp을 사용하면 그런 단점이 있군요👀

Comment on lines 58 to 60
{fail
? createPortal(<Toast>{fail}</Toast>, document.getElementById("toast"))
: null }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요기는 삼항연산자 말고 && 연산자로 해주면 !fail일 경우를 입력 안해줘도 될거 같웅!


export default LogIn;

const LogInBtn = styled(SignUpBtn)`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

야무지게 extending까지,,!!

background-color: white;
color: black;
font-weight: 600;
border: 0.2rem solid black;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정말 사소한 거지만 border는 예외적으로 단위로 px을 많이 사용해!! (보통 1px)
root값이 커진다고 테두리도 같이 두꺼워지는건 대부분의 경우에 디자이너들이 원하는게 아닐거라-!

Comment on lines 19 to 27
useEffect(() => {
if ( id && pw && checkPw && nickname
&& checkPw === pw && exist === false) {
setBtnState(true);
}
else
setBtnState(false);
}, [id, pw, checkPw, nickname, exist]
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우옹 이거 너무 유용한 정보다,, 나두 요 부분 useEffect로 구현했는데
반영해서 리팩토링 해봐여징🏃🏼‍♀️💨

};

//가입할 회원 데이터 전달
const signUphandler = async () => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요기 함수명에 카멜케이스가 적용되다 만 느낌,,?!?

<InputContainer>
<LoginInput>
<InputTitle>ID</InputTitle>
<Input type="text" name='id' id='id'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요기 input에 id 로 id를 넣어준게 어딘가 사용되는건감??
styled component도 살펴봤는데 딱히 관련이 없는거 같아서용

onKeyUp={(e) => setNickname(e.target.value)}/>
</LoginInput>
</InputContainer>
<SignUpBtn className={ btnState ? null : "inactive"}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요기 btnState에 따라 className 바뀌는 부분은
className = { !btnState && "inactive"}
요러케 바꿔줘도 될 거 같은데잉??

border-radius: 8px;
`;

export const Header = styled.header`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요기 header태그가 각 페이지에서 MyPage 이런거 나타낼때 쓰이던데
그 부분에 header태그를 쓰는게 적절할지는 한 번 생각해보면 좋을거 같옹!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants