Skip to content

pre-onboarding-Team-4/pre-onboarding-11th-1-4

Repository files navigation

📝 TODO 애플리케이션

원티드 프론트엔드 인턴십 1주차 과제

팀원

@Dain Kim @guen9310 @JYROH @Hyeondoonge @JBS @Sangeun Hwang
김다인 권부근 노준영 신현정 정범수 황상은

실행 방법

  1. 먼저 다음 명령어를 사용해서 로컬 환경으로 복사본을 가져옵니다.
git clone https://github.com/pre-onboarding-Team-4/pre-onboarding-11th-1-4.git
  1. 가져온 복사본으로 이동합니다.
cd pre-onboarding-11th-1-4
  1. 가져온 프로젝트의 종속성을 설치하세요.
npm install
  1. 이 프로젝트는 '.env'를 사용합니다. 다음 단계를 따라 .env를 설정해 주세요.
1. 루트 디렉토리에 '.env'파일을 생성 합니다.
2. 텍스트 편집기로 '.env' 파일을 엽니다.
3. '.env' 파일에 다음 변수와 해당하는 값을 입력하세요.

REACT_APP_API_END_POINT='https://www.pre-onboarding-selection-task.shop'
  1. 설치가 완료되었고, .env 설정이 완료 되었다면 다음 명령어로 프로젝트를 실행할 수 있습니다.
npm start

배포 링크

바로가기

기술 스택

팀규칙

1. 커밋 컨벤션

- feat: 새로운 기능 추가
- fix: 버그 수정
- docs: 문서 수정
- style: 코드 포맷팅, 세미콜론 누락, 코드 변경이 없는 경우
- refactor: 코드 리펙토링
- test: 테스트 코드, 리펙토링 테스트 코드 추가
- chore: 빌드 업무 수정, 패키지 매니저 수정
- design: 스타일 작업

2. 파일/폴더 구조

📦src
 ┣ 📂apis
 ┃ ┣ 📜auth.js
 ┃ ┗ 📜todo.js
 ┣ 📂components
 ┃ ┣ 📜AuthForm.jsx
 ┃ ┣ 📜Toast.jsx
 ┃ ┣ 📜ToastList.jsx
 ┃ ┣ 📜TodoForm.jsx
 ┃ ┣ 📜TodoHeader.jsx
 ┃ ┣ 📜TodoItem.jsx
 ┃ ┗ 📜TodoList.jsx
 ┣ 📂hooks
 ┃ ┗ 📜useToast.js
 ┣ 📂pages
 ┃ ┣ 📜NotFound.jsx
 ┃ ┣ 📜SignIn.jsx
 ┃ ┣ 📜SignUp.jsx
 ┃ ┗ 📜Todo.jsx
 ┣ 📜App.jsx
 ┣ 📜GlobalStyle.jsx
 ┣ 📜index.jsx
 ┣ 📜router.jsx
 ┗ 📜ToastContext.jsx

3. Style 컨벤션

- Styled-Component를 활용한 스타일링
- style.js를 따로가져가지 않고, 각 jsx 컴포넌트 하단부에 스타일 작성

서비스 소개

1. 기능

  • 회원가입
  • 로그인 기능
  • Todo: 추가, 수정/취소, 삭제 기능
  • 직접 만든 Toast UI를 활용한 사용자 피드백
  • 토큰 유무에 따른 리다이렉트 기능
로그인 회원가입
Alt text Alt text
회원가입(유효성검사) 투두
Alt text Alt text

2. Best Practice

모든 팀원들이 참여하여 중심 기능을 구현하는 최선의 방법들을 선정했습니다.


📌 Todo 컴포넌트 Best Practice 선정

  • useReducer를 활용한 Todo 상태 관리

❓선정이유

//리듀서 선언
function todoReducer(state, action) {
  switch (action.type) {
    case 'CREATE':
      return state.concat(action.todo);
    case 'TOGGLE':
      return state.map((todo) =>
        todo.id === action.id ? { ...todo, isCompleted: !todo.isCompleted } : todo
      );
    case 'UPDATE':
      return state.map((todo) =>
        todo.id === action.payload.id ? { ...todo, ...action.payload } : todo
      );
    case 'GET':
      return [...action.todos];
    case 'DELETE':
      return state.filter((todo) => todo.id !== action.id);
    default:
      return [];
  }
}

//리듀서 사용
const createToast = useToast();
const [state, dispatch] = useReducer(todoReducer, []);

const getTodos = async () => {
  try {
    const { todoList, message } = await getTodoList();

    dispatch({ type: 'GET', todos: todoList });
    createToast({ message, type: 'success' });
  } catch (error) {
    createToast({ message: error.message, type: 'warn' });
  }
};
  • Todo라는 상태를 반복되는 여러번의 state로 관리하는 것은 비효율적이었습니다.
  • 기존의 구현 방식은 여러번의 setState가 남발되었고 코드도 상당 부분 중복됐었습니다.
  • 따라서 하나의 useReducer를 활용한 Todo 관리가 효율적이라는 결론에 도달했고 미리 만들어둔 dispatch type에 따라서 간단하게 setState를 수행할 수 있었습니다.

📌 Route Best Practice 선정

  • loader를 활용한 Route단에서의 권한 미들웨어 처리

❓선정이유

  • 이번 과제에 있어서 권한(토큰의 유무)에 따라서 리다이렉트 처리를 하는 것이 핵심이었습니다.
  • 대부분의 팀원들은 다음과 같이 작업했었습니다.
useEffect(() => {
  if (!localStorage.getItem('accessToken')) {
    navigate('/signin');
  } else {
    getTodoList();
  }
}, []);
  • 특정 페이지에 컴포넌트(페이지)가 마운트됐을때 권한을 확인하고, 리다이렉트를 시키는 로직이었습니다.
  • 그러나 해당 로직은 몇가지 문제가 존재합니다
    • useEffect의 실행 타이밍은 컴포넌트 렌더링 이후이다. 따라서 불필요한 컴포넌트 렌더링이 이루어진다
    • useEffect로 처리를 안해도, 불필요하게 해당 파일을 load하고 읽는 과정이 소모된다.
  • 따라서 저희는 페이지 라우팅 이전에 이런 권한 체크의 필요성을 느꼈고 React-Router-Dom v6loader라는 기능을 활용하여 라우트 단계에서의 권한 처리를 수행하였습니다.
const privateMiddleware = () => {
  const jwt = localStorage.getItem('access_token');
  if (jwt) {
    return true;
  }
  return redirect('/signin');
};

const publicMiddleware = () => {
  const jwt = localStorage.getItem('access_token');
  if (jwt) {
    return redirect('/todo');
  }
  return true;
};

const route = [
  {
    path: '',
    loader: () => redirect('/signin'),
    errorElement: <NotFound />,
  },
  {
    path: '/todo',
    element: <Todo />,
    loader: privateMiddleware,
  },
  {
    path: '/signin',
    element: <SignIn />,
    loader: publicMiddleware,
  },
  {
    path: '/signup',
    element: <SignUp />,
    loader: publicMiddleware,
  },
];
  • loader는 컴포넌트가 생성전에 특정 작업을 수행하게 해줍니다. 따라서 컴포넌트 접근 전에 권한 체크를 privateMiddlewarepublicMiddleware로 수행하여 불필요한 컴포넌트 로드와 렌더링을 사전에 차단하였습니다.

📌 Toast UI Best Practice 선정

  • Context API를 활용한 전역 Toast UI 배열 상태 관리

❓선정이유

  • 팀원들은 모두 특정 동작(api 호출이나 localStorage 비우기)에 대한 유저 피드백이 있으면 좋겠다는 의견이 있었습니다.
  • 사전과제 진행시 Toast UI 라이브러리를 사용한 팀원도 있었고 직접 만든 팀원도 있었습니다.
  • 이번 기회에 제대로 Toast UI를 만들어보면 좋겠다는 의견이 나왔고 개발에 착수했었습니다.
// Toast 선언
export default function Toast({ children }: { children: ReactNode }) {
  return createPortal(
    <Styled.Toast>{children}</Styled.Toast>,
    document.getElementById('toast') as HTMLElement
  );
}

// Toast 사용(각 컴포넌트에서 다음과 같이 호출)
const [toast, setToast] = useState({ message: '', index: 0 });
setToast((toast) => ({ message: result.message, index: toast.index + 1 }));
  • 기존에 개발된 Toast에는 여러가지 문제가 있었습니다.

  • Portal을 이용한 root와의 분리작업은 좋았으나

    • Toast를 최대 한개만 호출 가능하고
    • Toast를 쓰려는 컴포넌트에서 매번 State 생성과 setToast작업이 필요하다는 점이었습니다.
  • 따라서 이부분에 개선점을 두고 작업하였습니다

export const ToastContext = createContext(null);

export default function ToastsContextProvier({ children }) {
  const [toasts, setToasts] = useState([]);
  const data = useMemo(() => [toasts, setToasts], [toasts]);

  return (
    <ToastContext.Provider value={data}>
      {children}
      {createPortal(
        <ToastList>
          {toasts.map(({ message, type }, idx) => (
            <Toast type={type} key={idx}>
              {message}
            </Toast>
          ))}
        </ToastList>,
        document.getElementById('toast')
      )}
    </ToastContext.Provider>
  );
}
  • 우선 여러개의 Toast를 다루기 위해 배열로 상태를 관리하였습니다. 이 배열은 ToastContext.jsx에서 전역 관리하여 각 페이지에서 무분별하게 state를 생성, 관리하지 않게 수정하였습니다.
  • 해당 작업을 진행하기 위해 Context API를 활용하였습니다.
  • 이상태에서 Toast를 사용하려면 Context로부터 배열을 꺼내고, 추가하는 작업이 필요했습니다.
    • 이 작업은 Toast가 필요한 모든 파일에서 반복될 것이기에, custom hook을 활용하여 추상화하기로 했습니다.
export default function useToast() {
  const toastContext = useContext(ToastContext);

  if (!toastContext) throw new Error('Toast provider를 추가해주세요');

  const [toasts, setToasts] = toastContext;

  const createToast = (toast) => {
    setToasts([...toasts, toast]);
  };

  return createToast;
}
  • 위와 같은 useToast훅을 생성하여 커스텀 훅으로 편하게 사용 가능하도록 작업하였습니다.

📌 Axios 사용 Best Practice 선정

  • axios instance와 interceptor를 활용한 공통 헤더 설정, 에러 처리

❓선정이유

  • 팀내에서 api를 대부분 axios를 활용하여 구성하였었습니다.
  • 이때 반복되는 api header 설정이 많았는데 이를 최소화하기 위한 axios instance와 interceptor의 활용이 좋아보였습니다.
const instance = axios.create({
  baseURL: `${process.env.REACT_APP_API_END_POINT}/todos`,
  headers: {
    'Content-Type': 'application/json',
  },
});

instance.interceptors.request.use(
  (config) => {
    const accessToken = localStorage.getItem('access_token');
    const newConfig = config;
    if (accessToken !== undefined) {
      newConfig.headers.Authorization = `Bearer ${accessToken}`;
    }
    return newConfig;
  },
  (error) => {
    return Promise.reject(error);
  }
);

instance.interceptors.response.use(
  (response) => {
    return response;
  },
  (error) => {
    const { data } = error.response;
    if (!data.message) {
      return Promise.reject(new Error('알 수 없는 에러가 발생했습니다.'));
    }
    return Promise.reject(new Error(data.message));
  }
);
  • axios instance를 생성하여 baseUrl과 content-type을 공통적으로 명시해줬습니다.
  • request interceptor를 활용하여 access_token을 동적으로 주입하였습니다.
    • 이를 통해 api호출시의 안전장치와 auth인증을 컴포넌트로부터 분리할 수 있었습니다.
  • 또한 response interceptor에서는 error를 공통적으로 가로채서 api에 전역적으로 방어로직을 추가할 수 있었습니다.

📌 제어컴포넌트 vs 비제어 컴포넌트

  • state를 활용한 제어 컴포넌트 방식 사용

❓선정이유

  • 로그인/회원가입 Input 기능 구현 중 과연 Input 상태관리가 필요한 가에 대한 궁금증이 팀내에 존재하였습니다.
  • state를 활용한 제어 컴포넌트와 DOM ref를 활용한 비제어 컴포넌트 중 어떤 방식이 좋은가에 대한 의논이 생기게 되었습니다.
  • 우선 비제어 컴포넌트 이용시, 상태관리 없이 코드를 더 간결하게 작성할 수 있는 이점이 있었습니다. 상태 관리 없이 submit 단계에서의 DOM ref를 통하여 값을 불러오기만 하면 됐기 때문입니다.
// 비제어 컴포넌트 활용
 const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const $input = event.target as AuthFormEventTarget;
    const email = $input.email.value;
    const password = $input.password.value;

    const res = await Api.signIn({ email, password });
    if ('message' in res) {
      setToast((toast) => ({ message: res.message, index: toast.index + 1 }));
    } else {
      localStorage.setItem('access_token', res.access_token);
      naviagte(TODO_URL);
    }
  };
  • 그러나 요구사항을 분석해본 결과 이메일, 비밀번호 field의 유효성을 검사하여 에러를 출력하거나, button의 활성화 상태를 변경하는 작업이 필요했습니다.
  • 그리고 이 작업은 필드 값들을 상태를 이용해 관리하면 쉽게 구현할 수 있었습니다.
  • 즉 실시간으로 검증이 필요한(validation)값들을 다룰때 제어 컴포넌트가 훨씬 유리하다는 결론에 도달하였고 useState를 이용한 onChange상태관리를 사용하였습니다.
// 제어 컴포넌트 활용
const [auth, setAuth] = useState({
  email: '',
  pw: '',
});

const emailOnChange = (e) => {
  const emailInput = {
    ...auth,
    [e.target.name]: e.target.value,
  };
  setAuth(emailInput);
};

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published