개발일지/타임어택 개발 - 사람인

타임어택 개발 프로젝트 - 사람인 개발일지 (1)

JSICODE 2025. 5. 18. 01:38

개발 기간

2025-05-17 ~ 2025-05-20 (4일)

 

개발일

2025-05-17

 

Github

https://github.com/SanginJeong/saramin/tree/develop

 

GitHub - SanginJeong/saramin

Contribute to SanginJeong/saramin development by creating an account on GitHub.

github.com

 

 

개발내용

Route설정, Layout (nav), loginPage, registerPage 의 디자인 개발

 

 

1. Route 설정

import { Routes, Route } from 'react-router'
import './App.css'
import AppLayout from './layout/AppLayout'
import HomePage from './pages/homePage/HomePage'
import RegisterPage from './pages/registerPage/RegisterPage'
import LoginPage from './pages/loginPage/LoginPage'

function App() {

  return (
    <>
      <Routes>
        <Route path='/' element={<AppLayout/>}>
          <Route index element={<HomePage/>}/>
          <Route path='/login' element={<LoginPage/>}/>
          <Route path='/register' element={<RegisterPage/>}/>
        </Route>
      </Routes>
    </>
  )
}

export default App

 

일단 모든 페이지가 AppLayout에 포함되어있는 구조로 짰고, AppLayout에서 페이지별로 Outlet을 렌더링 하는 구조로 짰다.

 

추후에 페이지가 추가될 때마다 Route는 추가할 것

 

2. AppLayout (nav)

 

import { Outlet } from 'react-router'
import NavTop from './nav/NavTop'
import NavMenu from './nav/NavMenu'
const AppLayout = () => {
  return (
    <>
      <nav className='shadow-md flex justify-center mb-8'>
        <div className='w-[80%] relative'>
          <NavTop/>
          <NavMenu/>
        </div>
      </nav>
      <div className='flex justify-center'>
        <div className="w-[80%]">
          <Outlet/>
        </div>
      </div>
    </>
  )
}

export default AppLayout

 

페이지를 전체적으로 80% 정도의 너비만 사용해서 페이지의 중앙에 컨텐츠들이 오게끔 하였다.

 

폴더안에 nav폴더를 만들어서 navTop과 navMenu 들로 나눴다.

 

살펴보기전에 최종 결과 사진부터 보고 뜯어보자.

 

기본 Navbar

 

SearchUI 컴포넌트가 열렸을 때

        <div className='w-[80%] relative'>
          <NavTop/>
          <NavMenu/>
          {isOpenSearchUI && <SearchUI/>}
        </div>

 

여기서 SearchUI 컴포넌트의 위치를 처음에는 AppLayout에 아래와 같이 놔뒀었지만 이러면 SearchUI가 열렸을 때 NavMenu 까지 덮어버리게 된다. 내가 하고싶은 것은 SearchUI가 열렸을 때도 NavMenu는 보여줘야하기 때문에 NavTop 안으로 옮겨줬다.

 

 

navTop

import React, { useState } from 'react'
import { Link } from 'react-router'
import { useSearchStore } from '../../store/useSearchStore'
import SearchUI from '../../common/searchUI/SearchUI'

const NavTop = () => {
  const {setSearchUI, isOpenSearchUI} = useSearchStore();
  
  return (
    <div className='flex py-4 relative top-0'>
      <div className='flex gap-4 flex-[8] items-center'>
        <div className='flex-[2]'>
          <Link className='text-[24px] text-blue-800 font-bold' to='/'>Saramin</Link>
        </div>
        <form className='relative flex-[10]' onClick={setSearchUI}>
          <i className="fa-solid fa-magnifying-glass absolute cursor-pointer left-2 top-[15px] text-blue-600"/>
          <input className='input-base px-7 cursor-pointer py-2 w-[60%]' type="text" placeholder='채용 공고 검색'/>
        </form>
      </div>

      <div className='flex-[4] flex items-center gap-4 justify-end'>
        <Link to='login' className='hover:text-blue-600 text-gray-600'>로그인</Link>
        <Link to='register' className='hover:text-blue-600 text-gray-600'>회원가입</Link>
      </div>
      {isOpenSearchUI && <SearchUI/>}
    </div>
  )
}

export default NavTop

 

 

navMenu

import React, { useState } from 'react'

const navMenuList = [
  {title: "menu 1", content: ['menu 1-1', 'menu 1-2', 'menu 1-3', 'menu 1-4']},
  {title: "menu 1", content: ['menu 1-1', 'menu 1-2', 'menu 1-3', 'menu 1-4']},
  {title: "menu 1", content: ['menu 1-1', 'menu 1-2', 'menu 1-3', 'menu 1-4']},
  {title: "menu 1", content: ['menu 1-1', 'menu 1-2', 'menu 1-3', 'menu 1-4']},
  {title: "menu 1", content: ['menu 1-1', 'menu 1-2', 'menu 1-3', 'menu 1-4']},
  {title: "menu 1", content: ['menu 1-1', 'menu 1-2', 'menu 1-3', 'menu 1-4']},
]

const NavMenu = () => {
  const [active, setActive] = useState(null);
  return (
    <div className='py-2 flex gap-4'>
      {navMenuList.map((menu,index)=>(
        <ul className='relative pr-4' onMouseOver={()=>setActive(index)} onMouseLeave={()=>setActive(null)}>
          <p className='hover:bg-blue-600 hover:text-white transition px-4'>{menu.title}</p>
          <ul className={`${index === active ? '' : 'hidden'} absolute -left-3 z-[100] border-1 rounded-md border-gray-400 shadow-md bg-white w-32 transition`}>
            {menu.content.map((subMenu)=>(
              <li className='text-gray-600 px-1 py-2 hover:bg-blue-600 hover:text-white transition'>{subMenu}</li>
            ))}
          </ul>
        </ul>
      ))}
    </div>
  )
}

export default NavMenu

 

드랍다운을 구현했다. 아직 드랍다운의 하위메뉴들을 뭘로 정할지 안정해서 임시 데이터들로 디자인을 구현했다.

 

드랍다운 하위메뉴들의 열리고 닫힘은 useState로 상태관리 해줬다. onMouseOver와 onMouseLeave를 이용해서 index와 active가 일치한다면 열리도록 설정했다.

 

 

 

3. loginPage

import React from 'react'
import { Link } from 'react-router'
const LoginPage = () => {
  return (
    <>
      <div className='flex'>
        <form className='flex flex-1 flex-col gap-4 '>
          <h1 className='self-center text-[32px]'>로그인</h1>
          <input type="text" className='input-base rounded-none p-2' placeholder='아이디'/>
          <input type="password" className='input-base rounded-none p-2' placeholder='비밀번호'/>
          <div className='flex justify-center gap-4 text-gray-400'>
            <Link>아이디 찾기</Link>
            <Link>비밀번호 찾기</Link>
          </div>
        </form>

        <div className='flex-1 flex justify-center items-center'>
          <Link to='/register' className='border-2 py-8 px-12 text-[24px] rounded-2xl bg-blue-600 text-white hover:bg-blue-400 transition'>회원가입</Link>
        </div>
      </div>

      {/* 에러바운더리 */}
    </>
  )
}

export default LoginPage

 

로그인 페이지는 딱히 헷갈렸던 로직이 없고 하던대로 잘 짜져서 스킵.

 

 

4. registerPage

import React from 'react'
import { useNavigate } from 'react-router'

const RegisterPage = () => {
  const navigate = useNavigate();
  return (
    <div className='flex flex-col pb-12'>
      <h1 className='text-[32px] mb-5'>회원가입</h1>
      <form className='flex flex-col gap-4'>
        <div>
          <p>아이디</p>
          <div className='flex gap-4 items-center'>
            <input type="text" className='input-base rounded-none w-[50%] px-2 py-1' />
            {/* 중복체크 시 비교해서 맞으면 체크표시 렌더링 오류있으면 에러문구 띄우기 */}
            <i className="text-green-400 fa-solid fa-check"></i>
            <button type='button' className='button-base bg-blue-600 text-white hover:bg-blue-400 transition'>중복체크</button>
          </div>
        </div>
        <div>
          <p>비밀번호</p>
          <div className='flex gap-4 items-center'>
            <input type="text" className='input-base rounded-none w-[50%] px-2 py-1' placeholder='특수문자, 숫자, 영어를 포함' />
            {/* onChage 시 비교해서 맞으면 체크표시 렌더링 오류있으면 에러문구 띄우기 */}
            <i className="text-green-400 fa-solid fa-check"></i>
          </div>
        </div>
        <div>
          <p>비밀번호 확인</p>
          <div className='flex gap-4 items-center'>
            <input type="text" className='input-base rounded-none w-[50%] px-2 py-1' />
            {/* onChage 시 비교해서 맞으면 체크표시 렌더링 오류있으면 에러문구 띄우기 */}
            <i className="text-green-400 fa-solid fa-check"></i>
          </div>
        </div>
        <div>
          <p>이메일</p>
          <div className='flex gap-4 items-center'>
            <input type="text" className='input-base rounded-none w-[50%] px-2 py-1' />
            <i className="text-green-400 fa-solid fa-check"></i>
          </div>
        </div>
        <div>
          <p>이름 / 나이 / 성별</p>
          <div className='flex gap-4 items-center'>
            <div className='flex gap-4 items-center w-[50%]'>
              <input type="text" className='input-base rounded-none px-2 py-1 flex-1' placeholder='이름'/>
              <select className='border-2 border-blue-600 p-1 flex-1'>
                {Array(42).fill(0).map((v,i)=>(
                  <option value={i+19}>{i+19}</option>
                ))}
              </select>
              <select className='border-2 border-blue-600 p-1 flex-1'>
                <option value="male">남성</option>
                <option value="female">여성</option>
              </select>
            </div>
            <i className="text-green-400 fa-solid fa-check"></i>
          </div>
        </div>

        <div>
          <p>주소</p>
          <div className='input-base rounded-none px-2 py-1 w-[50%] cursor-pointer text-gray-600 mb-1'>주소검색</div>
          <input type="text" className='input-base rounded-none px-2 py-1 w-[50%]' placeholder='상세주소'/>
        </div>
        
        <div className='flex justify-center gap-4'>
          <button type='submit' className='button-base bg-blue-600 text-white hover:bg-blue-400 transition min-w-30'>완료</button>
          <button type='button' onClick={()=>navigate('/login')} className='button-base bg-blue-600 text-white hover:bg-blue-400 transition min-w-30'>돌아가기</button>
        </div>
      </form>
    </div>
  )
}

export default RegisterPage

 

일단 디자인은 다 짜놨다. 체크표시 or 에러메시지가 뜨도록 할 것이다.

지금 딱 보니까 중복되는 코드들 (input 쪽)은 따로 input컴포넌트를 만들어서 props로 받으면 중복코드를 대폭 줄일 수 있을 것 같다. 이건 내일 하려고 한다. 합성컴포넌트 패턴을 사용할까 하다가 이 페이지에서 외에는 다른 용도로는 쓰지 않을 것 같아서 중복만 줄이기에는 적합하지 않을 것 같다. 합성컴포넌트는 모달 재사용 처럼 안에 내용이 많이 바뀌는데 여러곳에서 사용할 때 쓰면 좋다고 생각한다.

 

또, 주소검색은 우편번호 관련 api를 찾아서 모달로 띄울 예정이다.

 <select className='border-2 border-blue-600 p-1 flex-1'>
    {Array(42).fill(0).map((v,i)=>(
      <option value={i+19}>{i+19}</option>
    ))}
</select>

 

option이 42개나 필요해서 임의로 배열을 만들어서 반복문으로 렌더링 해주었다. 알고리즘 문제 연습할 때 배워놨던 Array.fill.map 을 이용했다.

 

 

구현하지 못한 것들

- navbar가 일정 scroll 이상 내려오면 navTop만 고정되도록 하고 싶다. 여기에 시간을 많이써서 오늘 진도를 많이 못나갔다. 내일도 도전한다.

 

 

내일 목표

- 로그인, 회원가입 (주소검색 api 잘 찾는게 중요) 백엔드 구현 및 테스트

- navbar scroll 고정 구현

- searchUI에서 지역선택, 직업선택 클릭시 focus (테두리 찐하게 누가봐도 이게 클릭됐다는게 보일정도)와 하위메뉴 나타나는 것 까지 디자인 (임시 데이터로)

- 모바일 화면 중간점검

1일차 깨우친 점

생각보다 4일안에 하기 정말 어려울 것 같다. 기간을 더 늘려야 할 수도 있다. 아직 api 문서 분석도 못했다... 그래도 navbar에 중요한 기능들 다 해놓으면 50% 라고 생각한다. 나머지 페이지에는 결국 공고들을 카드로 보여주는 식이 대부분 차지하기 때문에 쉬울 것 같다. 배포까지 공부하면.. 4일안에 할 수 있을지 모르겠지만 최대한 내 힘으로 해보자. 그리고 몰랐던 거 잘 체크하자. 내 이번 프로젝트의 목표는 axios, jwt 토큰 저장위치, 배포 3가지를 어느정도 머릿속에 정리하는 것이다. 가보자

 

그리고 css가 시간을 좀 잡아먹는다. tailwind가 아직 미숙한 부분도 있지만 근본적으로 내가 몰랐던 디테일한 것도 많았다.

1. absolute와는 다르게 fixed를 해버리면 부모요소의 크기를 생각하지 않는다.

2. sticky의 조건들 (3가지를 지키지 않으면 sticky가 작동하지 않음)