원티드 프리온보딩 기업과제#3

수박·2021년 8월 13일
2

원티드프리온보딩

목록 보기
2/3
post-thumbnail

과제설명

이번 과제는 자란다의 기업과제이다.

구현해야 할 사항은 크게 다음과 같았다.

  1. 회원가입 / 로그인 - 유효성체크
  2. 관리자계정에서 사용자 검색, 권한 수정, 페이지네이션
  3. 권한에 따른 페이지 접근 분기

2번 기능을 본인 포함 3명의 팀원과 페어프로그래밍으로 진행했다.

사용자데이터를 필터링하고, 검색하는 기능이 서로 연결되는 부분이 많다고 생각되었고,

해당 부분에 대해 설계 후 기능분담하는쪽 || 같이 구현해보지 못했던 것들을 함께 하는 방법

후자의 방법으로 진행했다.


1. 필터링, 검색기능 :: 담당부분

기능시연 영상

권한필터와, 검색기능 코드

//Admin.js
const [searchConditions, setSearchConditions] = useState({
    searchType: "name",
    condition: { whole: true, teacher: false, parents: false, admin: false },
  });

//AuthFilter.js
// * 토글 이외의 기능은 고정값으로 update하기에 여기엔 작성하지 않았습니다.
const toggleAuthFilter = (filterType) => ({
    searchType: searchType,
    condition: {
      ...condition,
      whole: false,
      [filterType]: !condition[filterType],
    },
  });

//SearchBar.js
const handleSearchKind = (e) =>
    setSearchConditions((prev) => ({ ...prev, searchType: e.target.value }));

const onKeyPress = (e) => {
    if (e.key === "Enter") {
      handleSearchClick(searchKeywordRef.current.value);
    }
  };
const clearSearchKeyword = (e) => {
    handleSearchClick();
    searchKeywordRef.current.value = "";
};

조건을 어떤 방식으로 관리할지에 대해서 많은 대화가 오갔었고

다양한 방법으로 먼저 구현을 진행하고 제일 상위에서 state를 관리하고 하위에서 변경하는 방식으로 진행했다. 

검색시 이름인지, 이메일인지 검색기준을 확인하는 searchType, 권한별 필터를 위한 condition을 부모의 state로 두었다.

검색어 입력시 검색어를 state로 두어 이벤트마다 새로 갱신하는 방법과 ref으로 값을 관리해

클릭 이벤트가 발생했을 때 해당 값만 전달해줄지에 대해 논의했다. 전자는 이미 경험해보았던 것이라 후자의 방식으로 진행했다.

  • 상태변화에도 리랜더가 되지않는 ref의 사용예시를 하나 알게되었다.

데이터 필터 코드

//filter
const search = (searchKeyword = searchKeywordRef.current.value) => {
    if (searchKeyword)
      return users.filter(
        (item) =>
          ((item.authority === 2 && parents) || //
            (item.authority === 1 && teacher) || // 중복
            (item.authority === 0 && admin) || // 
            whole) &&
          item[searchType] === searchKeyword
      );
    return users.filter(
      (item) =>
        (item.authority === 2 && parents) || // *
        (item.authority === 1 && teacher) || // * 중복
        (item.authority === 0 && admin) ||  // *
        whole
    );
  };

검색어와 선택한 필터조건에 따라 출력데이터가 변하는 코드인데, 한눈에 보다시피 중복된 코드가 존재한다.

좀 더 나눠보면 어떨까

const search = (searchKeyword = searchKeywordRef.current.value) => {
    if (searchKeyword){
      return userDataFilterWithSearchKeyword(searchKeyword);
    }
    return userDataFilterNoSearchKeyword();
  };

const userDataFilterWithSearchKeyword = (searchKeyword) => {
        return users.filter((item) =>
          filterSearchConditions(item) &&
          item[searchType] === searchKeyword
      );
}
const userDataFilterNoSearchKeyword = () => {
        return users.filter((item) =>
          filterSearchConditions(item)
      );
}

const filterSearchConditions = (item) => {
  return (item.authority === 2 && parents) || 
            (item.authority === 1 && teacher) ||
            (item.authority === 0 && admin) || 
            whole)
}

코드가 길어졌다. 그러나 기존의 코드는 한줄 한줄 두가지 분기에 대한 조건을 전부 확인해야했지만,

조건함수를 따로 뺀 결과 공통되는 부분을 한번에 확인할 수 있고, 함수의 이름도 조금 더 명확해진 것 같다. 재사용성을 생각해 파일 분리까지 하면 더욱 😋 (필터가 재사용될 일이 있을진 모르겠지만 ?)

프론트엔드에서도 데이터를 처리하는 로직을 다루는 작업이 큰 비중을 차지하는지 확실히는 모르겠으나, 폴더구조와 코드분리에 있어서 좋은 연습이 되가고 있는 것 같다.


2. 페이지네이션 :: 담당부분****

페이지네이션은 구글처럼 총 10개의 페이지를 고정으로 보여주는 방식으로 구현했다.

문제풀이하는 것과 다를게 없어서 재밌게 작업할 수 있었다.

~로직은 일반적인 산수의 느낌이 강해 해당 포스트엔 작성하지 않았다.~

백엔드로부터 offset이나 pageSize을 받고 url에 따라 페이지당 데이터를 불러오는 방식이 아닌,

해당 과제에서는 모든 것을 클라이언트에서 작성해야했기에 실제 백엔드와 데이터통신을 진행했을 땐 해당 방식으로 구현하지 않을 것 같다.

파일당 기능을 분리하기 위해 state를 나누어서 관리하자고 제안했고 이를 기반으로 진행했다.

state를 서로 공유하는 상황에서 구조적으로 깊어질 수밖에 없을 때 state관리가 복잡해져서 작성해야할 코드량이 상당히 증가했다.

상위에서 검색조건state를 관리하고 하위에서 이를 변경해 출력되는 데이터를 가공했는데 제일 상위에서 검색조건변경까지 다뤘다면 props로 전달하는 경우는 적어지겠지만 그에 따른 코드량이 더욱 상당해져 해당 파일에서 무슨기능을 하는지 정확히 파악하기 힘들었을 것이다.

💡 해당과제에서 전역상태관리를 어디서 해야할지, 왜 해야할지에 대한 필요성을 크게 체감하지 못했었는데 검색조건에 대해서는 전역으로 두었다면 props전달을 아예 하지 않아도 되므로 파일별 기능이 더욱 확실히 구분되고 보기에도 쉬운 코드가 작성되었을 것이라고 생각한다.

아래는 담당부분외 코드에 대한 개인적인 생각을 정리하므로 내용이 길 수 있습니다. 회고는 제일 마지막에 작성해두었습니다.

🧐 코드를 보며 들었던 개인적인 생각이므로 제 생각이 옳지 않을 수 있습니다.



3. 내가 담당했던 부분외 기능

분업해서 과제를 진행했기에 다른 분들이 작성한 코드를 보는 시간을 가졌다.

3.1 권한 라우터 with HOC

//App.js
const App = () => {

  return (
    <Container>
      <Routes />
    </Container>
  );
};

  • App.js에서 Router를 관리하지않고 Routes파일을 따로두어 기능상 분리를 진행했다. 또한 라우터경로를 상수로 관리해 가독성이 높아졌다.

🤔 해당 경로를 확인하기 위해 상수파일로 접근해야하는 경우가 있으므로 주석을 통해 해당 상수값을 정리해놓는 방법은 어떨까 ?

HOC에서 Props 전달방법 with 커링패턴

  • Route에서 전달되는 component가 받는 props는 match, location, history가 있다.
  • history에 새로운 entry를 추가해 해당하는 path의 컴포넌트를 랜더링하기 위해 HOC내 컴포넌트에 props를 전달하였다.  react-router-dom- Route props
  • props전달방식에 의문이 생겼고 팀원분께 (주말인데도 불구하고) 질문을 드렸다.
  • react-router의 Route가 받는 Component는 3가지의 props를 전달받게 되는데, HOC로 감싼 컴포넌트에 history를 전달하기 위해 해당 코드를 작성하셨다고 한다.
  • 간단하게 풀면 이중적으로 props를 전달하기 위함이다.(Route => HOC => Wrapped Component)
  • 커링의 형태로서 콜백 또는 함수를 반환하는 방식으로 props를 전달받아 사용할 수 있다. 커링이 뭔데 ?

👉 커링예제

함수를 반환해서 2번째 인자를 넣어야 반환되도록하는 패턴으로 반환결과의 시점을 제어할 수 있다.

이 방법을 HOC에 적용해본다면 다음의 방식이 된다. 그러나 해당 방법은 에러를 야기시킨다.

    //props를 전달받는 방법 (커링)
    const AuthorityControl = (Components, option, authLevel) => ({ history }) =>
    {
    	..중략, return <Component/>
    }
  • AuthorityControl이 반환하는 것도 Component고, 그 안에 Wrapped될 컴포넌트가 들어간다.
  • wrapped될 컴포넌트에 Route의 history를 props로 전달해주어야하므로 (WrappedComponent) => ( { history } ) => 의 패턴으로 작성할 수 있지만 이는 에러를 발생시킨다.
  • 리액트의 hooks는 콜백내부에서 사용될 수 없다. 함수의 최상단레벨에서 작성되어야한다.
  • 따라서 hooks를 사용하는 컴포넌트에 인자를 전달하고자할 때 커링패턴이 아닌, 컴포넌트함수를 반환하도록 작성하자

따라서 팀원께서 작성하신 코드는 해당 에러를 피하기 위해 화살표 함수를 반환하는 방식으로 사용하신 것!

✅ 기존 구현사항(화살표함수)에서 다른 방법(함수)으로 구현해보았다. -> 기존 코드에서 return을 제거할 수 있다.

const AuthorityControl =
  (Components, option, authLevel) =>
    return function WrappedComponent ({history}){
    useEffect(()=>{});
        return <Component .../>
  }

💡 history객체를 HOC내에서 props로 전달받기 위해서는 커링패턴을 사용할 수있다. 그러나 hooks가 포함된 컴포넌트에서는 사용할 수 없다. 왜냐하면 hooks는 콜백내에서 사용될 수 없기에 내부함수형태로 작성해주어야한다.
React Hook "useState" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function 참고블로그

컴포넌트의 상태값들은 컴포넌트를 키로 하는 배열에 순서대로 저장되기 때문에, hook을 조건문이나 일반 javascript 함수 안에서 사용하게 된다면, 맨 처음 함수가 실행되었을 때 저장되었던 순서와 맞지 않게 되어 잘못된 상태를 참조하게 될 수 있다

출처


3.2 Modal with Portal

Modal을 Portal을 사용해 구현하셨다.

Portal?

UI를 어디에 랜더링시킬지 DOM을 사전에 선택한 후 부모 컴포넌트 DOM 계층 밖에있는 DOM노드로 자식을 랜더링하는 방법이다.

부모컴포넌트의 하위가 아닌, 시각적으로 바깥에 출력하고 싶을 때 사용한다.

공식문서의 예로는 root와 같은 레벨에 modal-dom을 미리 작성해두어 해당 element를 선택해 그 하위로 내용을 출력시키는 방법이 있다.

index.html을 변경시키지 않고 하위컴포넌트에 Portal이 작성되어있어 변경점을 공유했고

index.html의 root와 같은레벨의 요소를 추가해 그 하위로 모달을 랜더링하도록 변경했다.

팀원분께서  index.html을 변경시키지 않는 방향으로 작성하기 위해 하위에 그대로 사용하였다고 하셨는데, 꼭 변경점이 없어야하는가가 궁금하다. 물론 side-effect가 생기도록 변경하는 것은 당연히 안되겠지만 div하나로 생기는 변수가 존재할까 ? 다른 방법이 존재할까 ?

🤔 index.html을 변경시켜서 생기는 side-effect가 존재할까 ? 변경하지 않으면 portal을 사용하는 의미가 있을까 ?

💡 다른 예제를 보았을 때, body태그에 아예 modal을 붙였는데 이 방법이 의문점을 해소해주는 것 같다.
index.html을 변경하지않고, root와 같은레벨에 컴포넌트를 붙일 수 있다. 예제

**"그러나, DOM조작을 피해야한다라고 강조해주셨던 것을 생각해보면
모달이 생성될 때마다 DOM을 추가하는 방식도 괜찮은지가 궁금하다"

**
꼭 같은 레벨에 붙이지 않아도 될 것같다. 부모의 size에 따라 제대로 결과가 보여주지 않으면 제대로 보여줄 수 있는 컴포넌트에 붙여도 된다고 생각한다.

포탈을 생성해두었다면 다른 곳에서도 재사용 가능할 수 있도록 전체 레벨에 두는 편이 좋을 것 같다는 생각이다.

이런 근거를 들고 팀원분들과 소통하고 코드수정을 진행했어야했는데 공식문서의 예제만을 기반으로 소통했었다. 그리고 나의 견해를 전달했어야했는데 그러질 못했다. - 아쉬운 부분 -


3.3 로그인 / 로그아웃시 프로필사진 랜더링

//기존코드
const Header = () => {
  const location = useLocation();
  const [logged, setLogged] = useState(false);
  const userData = loadLocalStorage(LOGGEDIN_USER);

  useEffect(() => {
    userData ? setLogged(true) : setLogged(false);
  }, [userData]);
  //생략
  return {logged ? <LogoutButton /> : <LoginButton />}
}

// 수정 후 코드
const Header = () => {
  const location = useLocation();
  const [userData, setUserData] = useState([]);

  useEffect(() => {
    setUserData(loadLocalStorage(LOGGEDIN_USER));
  }, [location]);
//  ...생략
return  {userData ? <LogoutButton /> : <LoginButton />}
}

Header에 유저가 로그인했을 때 프로필사진이 위치하도록 작성해주셨다.

기존의 코드에서 location이 아예 사용되지 않은채로 있어서 의문을 가졌었다.

그러나 location = useLocation() 이 코드를 지우면 제대로 동작하지 않았다.

🤔 사용하지않는 location을 없애면 동작하지 않는 이유가 뭘까?

🤔 userData를 state로 관리하지 않아서 상태값을 저장될 수 없고 location을 제거하면 path가 변경되어도 변경점을 감지할 수 없기에 리랜더를 될 수 없는 것인가 ?

🤔 데이터를 fetch하는 함수가 header컴포넌트를 랜더될 때마다 호출되므로 state로 관리하도록 변경했는데 state로 작성하지 않으신 이유가 궁금하다.

💡 반환되는 location객체는 현재 URL을 대표하고, useState처럼 생각할 수 있다. -> URL이 변경될 때마다 새로운 값을 반환한다.react-router docs- useLocation에서..의 예제를 보면,
location을 의존성 배열에 추가하고, path가 변경되었을 때마다 유저데이터를 검증하는 bool값의 state로 랜더링조건을 변경하면 좋지 않을까 싶다.


3.4 컴포넌트 재사용

src/components/common의 구조에 자주 사용되는 컴포넌트들을 재사용하기 위해 input, button을 분리되어있다.

사용되는 모든 Input 컴포넌트에 대해서 재사용하기 위해 해당 방식으로 작성하신듯하다.

🤔 modalType을 props로 전달하고 하위에서 분기처리하면 조금 더 깔끔해질 것 같다. 로직도 분리해줄 수 있을 것 같다.

🤔 모두 함수형 컴포넌트로 작성되어있는데 Button은 클래스형으로 되어있다. 일관성있지 않은듯한데 이렇게 작성하신 이유가 궁금하다.

🤔 Signup페이지에 500줄가량의 코드가 작성되어있다. 모든 로직과 컴포넌트가 들어가있는데 분리될 수 없었는지 궁금하다. .
-> 각 input에 대한 유효성검사를 위해서는 이 방법뿐인가 ? 생각이 든다.
근데 보니 분리될 수 있는 컴포넌트가,, 뭐가 있을까. 모두 회원가입이라는 공통된 주제를 지니고 있다.

유효성검사를 위한 state만 Page에 두고 true, false만 전달하도록 하는 편이 조금 더 좋을 것 같다.

💡 Page라는 폴더구조 특성상 form태그와 내부 로직은 따로 빼는게 좋다고 생각한다.


3.5 유효성검사

input에 입력되는 값을 받아 정규표현식함수로 깔끔하게 분리하셨다 👍

뭔가 글을 쓰다보니 팀원분들의 코드를 "평가"한 것 같은 뉘앙스인데

제가 담당하지 않았던 코드들을 보면서 "내가 이 부분을 맡았다면 이렇게 작성하지 않았을까 ? "하는

저의 주관적인 생각일뿐입니다.


찝어주신 관련 면접질문

  • HOC 란?
    • Higher Order - Component로 컴포넌트를 인자로 받아 새로운 컴포넌트를 다시 return해주는 함수
    • 코드의 반복을 줄이고, 로직과 뷰를 분리하기 위한 목적으로 사용됨.
    • with___으로 붙이는 것이 관행-
  • HOC를 어디에 쓸 수 있을지? 구체적인 예
    • 반복되는 것을 줄일때 - 웹 요청이 반복될 때 전달하는 endpoint만 바꿔서 전달하면 요청부분만 담당하는 컴포넌트로 만들 수 있다! 예제 - https://velopert.com/3537

😋 과제 회고

내가 과정에서 기대했던 협업을 이루어낸 것 같다.

요구사항에 따른 명세서작성 후 페이지디자인을 뽑아내고 이를 기반으로 한 기능분담github issue발행,

작업진행에 따른 칸반보드로 일정관리, 스크럼회의를 통한 작업사항공유

그리고 git flow를 통한 형상관리를 통해 짧은 시간내에 좋은 협업과정을 경험했다고 생각한다.

협업내용

스크럼회의1

스크럼회의2

스크럼회의3

스크럼회의4

git-flow 참고 article

0개의 댓글