✨ 킨더그루

M1

👉🏻 서비스 소개

  • 선생님은 아이들의 등/하원 관리를 쉽고 간편하게!
  • 부모님은 아이들의 출결이력 확인 및 결석신청을 간편하게 !
  • 아이들의 소중한 순간을 볼 수 있는 갤러리까지!

✨ 안전하고 편리한 유치원 출결관리의 시작 킨더그루 입니다.

👉🏻킨더그루 이용해보기!
👉🏻킨더그루 팀 노션!
👉🏻킨더그루 프론트엔드 컨벤션

🐻 기능 구현

Clean desk with Dell display mockup (Mockuuups Studio)

간편한 소셜 로그인으로 유치원을 검색해 가입해보세요 !

M2

선생님이시라면 아이의 등하원 관리와 부모님께 보내는 알림톡 기능을 이용해보세요 !

M3

부모님이시라면 아이의 출결이력을 확인하고 결석도 간편하게 신청하실 수 있어요 !

M3

갤러리를 통해 아이들의 소중한 순간을 간직할 수 있어요 !

M4




🛠 프로젝트 아키텍쳐

M7




⚙ 기술 스택

✔ Frond-end



✔ Dev tools



📝 기술적 의사결정

기술 사용한 이유
Recoil 클라이언트 전역 상태관리로 Recoil을 사용했습니다.
저희 프론트 개발팀의 기술 도입 원칙이 "쉬운 사용성, 코드의 단순화, 효율성" 이었기 때문에 리액트와 친숙한 작동 방식으로 쉽게 사용할 수 있으면서 Redux보다 보일러 플레이트가 헌저하게 적고, 낮은 러닝 커브로 쉽게 프로젝트에 적용할 수 있다고 논의되어 사용하게 되었습니다. 라이브러리 자체의 모토가 "보다 React 스럽게" 인 만큼 사용함에 있어서 굉장히 친숙하고 편리했으며, 효율적인 상태관리를 할 수 있었습니다.
React-Query 서버사이드의 상태관리에는 React-Query를 사용했습니다.
기존의 Redux-toolkit-thunk 로 서버사이드의 데이터를 관리할 경우, 에러처리나 로딩 뷰 등에 대한 예외처리를 위해 작성해야 하는 보일러 플레이트가 길어져 코드의 양이 길어질 뿐 아니라, 반복되는 코드가 많다는 단점이 있었습니다. React-Query 는 팀의 기술 도입 원칙에 가장 적합한 서버 사이드 상태 관리 라이브러리 였으며, 로딩 뷰, 에러처리 등의 비동기 처리 및 에러 핸들링 로직을 적은 보일러 플레이트로 구현할 수 있었고 캐싱을 통해 효율적으로 데이터를 관리할 수 있었습니다.
styled-components 스타일링을 위해 Styled-components 를 사용했습니다. 부트캠프를 진행하면서 가장 오랜기간 사용했던 스타일링 도구이기에 숙련도가 가장 높고, 조건부 렌더링을 통한 유연한 스타일링이 강점이라고 생각하여 도입했습니다. 또한, style-reset을 사용함으로써 브라우저 간의 스타일 차이를 줄이고 일관된 디자인을 사용할 수 있도록 하여 default style 을 고려해야 할 필요 없이 일관된 스타일을 적용할 수 있도록 하였습니다. 추가적으로 ThemeProvider를 통해 프로젝트 전체에 일관된 색상과 폰트, 스타일 변수를 쉽게 관리할 수 있었고 font와 buttons 의 Variants 를 제작해 디자인 시스템을 운용함으로써 스타일드 컴포넌트 자체의 재사용성을 높이고 코드의 가독성과 유지관리를 용이하게 했습니다.

🔆 트러블슈팅

공통 버튼 컴포넌트 리펙토링
리팩토링 이전 코드
import styled, { css } from "styled-components";
import { lighten } from "polished";
import buttonVariants from "../../styles/variants/buttonVariants";

export const StyledButton = styled.button`
  display: inline-flex;
  width: max-content;
  height: max-content;
  justify-content: center;
  align-items: center;

  background-color: ${({ colorTypes, bgColor, theme, outlined }) => {
    if (outlined) {
      return bgColor ? bgColor : theme.color.white;
    }
    return colorTypes
      ? theme.color[colorTypes]
      : bgColor ?? theme.color.grayScale[50];
  }};

  border: ${({ outlined }) => (outlined ? "1px solid" : "none")};
  border-color: ${({ outlined, bgColor, theme, colorTypes }) =>
    outlined
      ? colorTypes
        ? theme.color[colorTypes]
        : bgColor
        ? bgColor
        : theme.color.grayScale[400]
      : null};

  color: ${({ outlined, theme, colorTypes, color }) =>
    color
      ? color
      : outlined
      ? colorTypes
        ? theme.color[colorTypes]
        : theme.color.grayScale[400]
      : colorTypes
      ? theme.color.white
      : theme.color.grayScale[200]};

  &:hover {
    background-color: ${({ colorTypes, bgColor, theme, outlined }) =>
      outlined
        ? bgColor
          ? lighten(0.1, bgColor)
          : theme.color.white
        : colorTypes
        ? lighten(0.1, theme.color[colorTypes])
        : bgColor
        ? lighten(0.1, bgColor)
        : lighten(0.2, theme.color.grayScale[50])};
  }

  ${({ disabled }) =>
    disabled &&
    css`
      background-color: ${({ theme }) => theme.color.grayScale[50]};
      color: ${({ theme }) => theme.color.grayScale[200]};
      opacity: 70%;
      pointer-events: none;
    `}

  ${({ buttonsTypes }) =>
    buttonsTypes === "State" &&
    css`
      color: ${({ theme, colorTypes }) => theme.color[colorTypes]};
      background-color: ${({ theme, colorTypes }) =>
        theme.color[`${colorTypes}_lighter`]};

      &:hover {
        color: ${({ theme, colorTypes }) => theme.color[colorTypes]};
        background-color: ${({ theme, colorTypes }) =>
          lighten(0.3, theme.color[colorTypes])};
      }
    `}

  ${({ buttonsTypes }) => buttonVariants[buttonsTypes] ?? ""}

  ${({ width }) =>
    width &&
    css`
      width: ${width};
    `}

  ${({ height }) =>
    height &&
    css`
      height: ${height};
    `}

  opacity: ${({ opacity }) => opacity};
  transition: background-color 0.3s ease-in-out;
  white-space: nowrap;
  cursor: pointer;
`;
리팩토링 이후 코드
import styled, { css } from "styled-components";
import { lighten } from "polished";
import buttonVariants from "../../styles/variants/buttonVariants";

const getBackgroundColor = (props) => {
  const { colorTypes, bgColor, theme, outlined } = props;
  if (outlined) return bgColor || theme.color.white;
  return colorTypes
    ? theme.color[colorTypes]
    : bgColor || theme.color.grayScale[50];
};

const getBorderColor = (props) => {
  const { outlined, bgColor, theme, colorTypes } = props;
  if (!outlined) return null;
  return colorTypes
    ? theme.color[colorTypes]
    : bgColor || theme.color.grayScale[400];
};

const getTextColor = (props) => {
  const { outlined, theme, colorTypes, color } = props;
  if (color) return color;
  if (outlined)
    return colorTypes ? theme.color[colorTypes] : theme.color.grayScale[400];
  return colorTypes ? theme.color.white : theme.color.grayScale[200];
};

const getHoverBackgroundColor = (props) => {
  const { colorTypes, bgColor, theme, outlined } = props;
  if (outlined) return bgColor ? lighten(0.1, bgColor) : theme.color.white;
  return colorTypes
    ? lighten(0.1, theme.color[colorTypes])
    : bgColor
    ? lighten(0.1, bgColor)
    : lighten(0.2, theme.color.grayScale[50]);
};

export const StyledButton = styled.button`
  display: inline-flex;
  width: max-content;
  height: max-content;
  justify-content: center;
  align-items: center;

  background-color: ${getBackgroundColor};
  border: ${({ outlined }) => (outlined ? "1px solid" : "none")};
  border-color: ${getBorderColor};
  color: ${getTextColor};

  &:hover {
    background-color: ${getHoverBackgroundColor};
  }

  ${({ disabled }) =>
    disabled &&
    css`
      background-color: ${({ theme }) => theme.color.grayScale[50]};
      color: ${({ theme }) => theme.color.grayScale[200]};
      opacity: 70%;
      pointer-events: none;
    `}

  ${({ buttonsTypes }) =>
    buttonsTypes === "State" &&
    css`
      color: ${({ theme, colorTypes }) => theme.color[colorTypes]};
      background-color: ${({ theme, colorTypes }) =>
        theme.color[`${colorTypes}_lighter`]};

      &:hover {
        color: ${({ theme, colorTypes }) => theme.color[colorTypes]};
        background-color: ${({ theme, colorTypes }) =>
          lighten(0.3, theme.color[colorTypes])};
      }
    `}

  ${({ buttonsTypes }) => buttonVariants[buttonsTypes] ?? ""}

  ${({ width }) =>
    width &&
    css`
      width: ${width};
    `}

  ${({ height }) =>
    height &&
    css`
      height: ${height};
    `}

  opacity: ${({ opacity }) => opacity};
  transition: background-color 0.3s ease-in-out;
  white-space: nowrap;
  cursor: pointer;
`;

문제

  • 요구사항의 추가가 어려움, 코드의 가독성이 크게 저하됨.

원인

  • 여러 속성값들을 한 줄에 연산하여 처리. 과도한 삼항연산자 사용. 최적화 되지 않은 코드.

개선 내용

  • 각 역할을 수행하는 함수를 분리함으로써 수정이 쉽고 명시적으로 역할을 확인할 수 있음.
  • 과도하게 중첩된 삼항연산자를 개선하여 코드의 가독성 증가.

전역 상태관리를 이용한 컴포넌트 관련 오류 개선
수정 사항
스크린샷 2023-04-17 16 04 40



스크린샷 2023-04-17 16 07 13

문제

공통 컴포넌트인 모달, 프로필 이미지 업로더가 중복으로 열리거나 중복된 데이터를 바인딩.

원인

전역 상태관리 시 공통된 상태값을 공유하고 있기 때문에 영향을 받는 컴포넌트가 전부 렌더링.

개선 내용

  • 고유한 ID를 추가하여 각 컴포넌트가 고유한 상태값을 가질 수 있도록 수정.
  • 영향을 받는 컴포넌트만 렌더링 함으로써 불필요한 렌더링 방지.

motion.div 와 styled-components 를 혼용시 Warning 메시지
오류 메시지

스크린샷 2023-04-17 21 23 12


문제

styled-components로 작성된 컴포넌트에 motion.div를 적용한 후, React 경고가 발생.

원인

Styled-components 만 사용했을 때는 문제되지 않았으나 DOM 요소와 직접적으로 연관이 있는 framer-motion를 적용했을 때, React가 카멜케이스로 작성된 사용자 정의 속성을 인식하지 못하고 브라우저에서 사용자 정의 속성을 추가할 때 소문자와 대시(-)를 사용한 케밥케이스를 사용하도록 권장하기 때문에 경고가 발생.

고려사항

그러나 실제로 속성은 인식되고 있으며, 경고는 DOM 요소에 대한 속성 작성 규칙과 관련된 것이다. 경고는 성능과 관련된 문제가 아니라 HTML 속성 작성 규칙에 관한 것이다. 따라서 성능상의 이슈는 없으며, 이 경고를 무시하고 넘어가도 큰 문제가 발생하지 않는다.

결론

모든 사용자 정의 속성의 이름을 케밥 케이스로 변경하는 것으로 해결이 가능하나, 프로젝트의 코드 컨벤션이 카멜 케이스를 지향하고 있어 일관된 코드 작성을 침해하며, 현재 개발 단계에서 모든 사용자 정의 속성을 케밥 케이스로 변경하는 것은 리소스가 크다고 판단. 또한, React 작성 규칙에 의한 Warning 으로 성능과는 연관이 없으므로 known issue 처리.




👻 킨더그루의 팀원들

역할 이름 구현내용
❤️ BE 홍예석 멤버 관리 페이지, 갤러리 페이지, 반 별 페이지, 유치원 생성, 글로벌 예외처리, 권한 분리, CI/CD
BE 이상훈 등하원 관련, 출결 관련, classes API, MySQL 프로시저 등록, 카카오톡 메세지 API, 테스트 코드 작성, 성능 최적화
BE 김근호 로그인 / 회원가입, 프로필 수정 API, SpringSecurity, Redis, Token 관리
BE 김현호 아이 프로필 CRU, 아이&부모 매칭, 부모 페이지 아이 조회 및 수정, 미세먼지 api
🧡 FE 백주원 등/하원, 출석부(일별,월별), 멤버 관리페이지, ReloadRoute ,전역로더/에러,공톰 컴포넌트 , 반응형 적용, 프레이머 모션 적용 ,미세먼지, 성능최적화
FE 황재연 학급관리 페이지, 갤러리 구현
FE 우주호 로그인/회원가입, 아이관리 페이지, 디자인 시스템(ThemeProvider, Variants), 공통 컴포넌트
DE 송규호 페이지 디자인, 로고 및 홍보 포스터 카드 제작