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

[점심 뭐 먹지 미션 Step 2] 윤생(이윤성) 미션 제출합니다. #64

Merged
merged 26 commits into from
Apr 18, 2023

Conversation

2yunseong
Copy link

안녕하세요 케빈!

이번 2단계 미션은 클래스형 컴포넌트를 함수형 컴포넌트로 변경하고, Custom Hook으로 분리하는 것 위주로 진행했어요.

클래스형 컴포넌트에서 함수형 컴포넌트로 변경하니 중복되는 코드가 많이 사라지는 경험도 하고 간결해짐을 느꼈어요..!!

다만 그만큼 componentDidMount, ~DidUpdate 등을 useEffect로 사용하다 보니 리액트 생명주기에 대해 공부를 좀 해봐야겠다 느꼈습니다!

이번 미션에서 Custom Hook을 분리할 때는 RestaurantListRestaurantDrawer에서 사용하는 로컬스토리지에서 데이터를 불러오는 로직이 같다고 생각해 재사용성 측면에서 분리하였습니다.

저번처럼 궁금한 부분은 댓글에 남겨두겠습니다! 잘부탁드립니다 :)

미션 구현 사이트

Copy link
Author

@2yunseong 2yunseong left a comment

Choose a reason for hiding this comment

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

저번처럼 질문 남깁니다!!

<button
type="button"
className="button button--secondary text-caption"
onClick={() => onToggleDrawer()}
Copy link
Author

@2yunseong 2yunseong Apr 16, 2023

Choose a reason for hiding this comment

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

onToggleDrawer는 App으로 부터 받은 녀석인데요..
다음과 같이 구현되어 props로 전달되어 집니다.

// App.tsx
const onToggleDrawer = (id: number = NO_SELECT_ID): void => {
    setDrawerSelectId(id);
    setIsOpenDrawer(!isOpenDrawer);
  };

네이밍이 이벤트핸들러 스럽긴 한데, 따로 한번 감싸주어야 할까요?

// RestaurantDetailDrawer.tsx
const onClick = () => {
  onToggleDrawer();
} 
// ... 코드 생략
      <button
        type="button"
        className="button button--secondary text-caption"
        onClick={onClick}

Choose a reason for hiding this comment

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

onToggleDrawer 외에 다른 이벤트가 없다면 굳이 안 감싸줘도 될거 같아요!

Comment on lines 6 to 9
type RestaurantProps = {
readonly restaurant: Omit<Restaurant, 'link'>;
restaurant: Omit<Restaurant, 'link'>;
onToggleDrawer: (id?: number) => void;
};
Copy link
Author

Choose a reason for hiding this comment

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

저번에 케빈께서 type을 공통적으로 빼면 어떠냐고 제안해주셨어요! 거기에 대한 제 생각입니다! props로 비슷하다고 생각해 저번
#33 (comment) 에 대한 답변을 듣고 싶어 이곳에 남깁니다!!

type 은 다른 파일에서 공통적으로 사용할 수 도있고, 확장해서 사용할수도 있어서 따로 빼놓는건 어떨까요?

저는 이런 App 컴포넌트의 상태를 명시해주는 건 오히려 외부 파일로 분리하면 App.tsx 를 읽을 때 번갈아서 봐야될 것 같다는 생각이 들어 함께 넣어 두었습니다. 또한 AppState는 App의 state가 가질 타입이라고 생각이 들어 다른 곳에서는 사용하지 않을 것 같아서 같은 파일에 넣어두었습니다! 이것에 대해 케빈은 어떻게 생각하시나요??

Choose a reason for hiding this comment

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

그런 이유라면 같은 파일에 둬도 좋을거 같아요!
하지만 저는 타입이 general하게 사용되는 타입이 아니고 해당 컴포넌트의 Props에서만 사용하기 때문이라면 같은 파일보단 같은 파일에 두지 않고, 뷰에 집중하기 위해 ~~~.type.tsx 이렇게 파일을 만들어서 나누어서 사용할 거 같아요! 윤생이 말한 방법은 ide에서 타입을 추론해주기 때문에 마우스 커서만 올려두면 알려줄것 같습니다!

Copy link
Author

Choose a reason for hiding this comment

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

ide를 이용할 수도 있겠네요! 또 하나 얻어갑니다!! 감사합니다!

Comment on lines 13 to 18
const filterByCategory = (restaurantList, category): Restaurant[] => {
if (category === '전체') return restaurantList;
return restaurantList.filter(
(restaurant) => restaurant.category === category
);
};
Copy link
Author

Choose a reason for hiding this comment

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

이런 계산하는 로직은 따로 분리하는게 나을까요? 저는 이 컴포넌트에서만 사용하는 로직이니까 같은 파일에 넣어두었는데, 케빈의 생각이 궁금합니다.

Choose a reason for hiding this comment

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

저도 if (category === '전체') 이런 조건이 있으니까 해당 컴포넌트에서만 사용하는 로직이 분명해서 해당 파일 근처에 두는게 좋을거 같은데, 지금 hook 을 배우고있으니까, hook 을 활용해서 관리해보면 어떨까요?

Choose a reason for hiding this comment

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

너무 잘 알고 계시겠지만, 활용할수 있는 힌트는 메인 코멘트에 적어 놓을게요~

Copy link
Author

Choose a reason for hiding this comment

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

한번 도전해보겠습니다~!

@JeongBin0227 JeongBin0227 self-assigned this Apr 16, 2023
@JeongBin0227
Copy link

다만 그만큼 componentDidMount, ~DidUpdate 등을 useEffect로 사용하다 보니 리액트 생명주기에 대해 공부를 좀 해봐야겠다 느꼈습니다!

class 의 생명주기를 많이 활용한 코드에서 function 형으로 바꾼 코드를 많이 보시면 도움되실거예요!
지금은 큰 영향 없지만 사이즈가 큰 서비스를 만들때 큰 도움이 되실거예요! (기획을 보면 컴포넌트가 ~~~ 어떨때등 자세히 기획이 나오는 경우가 많아서 생명주기를 자세히 알아야합니다)

@JeongBin0227
Copy link

재사용이 가능한 로직을 hook 으로 만들어서 사용하면 좋습니다.
예를 들어 자주 사용하는 api 호출을 예로 들어서 설명하면

같은 api를 사용해서 그리는 컴포넌트들이 있는데, 모든 컴포넌트마다 api 로직을 넣는것이 아니라

useGetApi 라는 파일을 만들고 (custom hook의 이름은 무조건 use로 시작해야합니다.)

export default function useGetApi() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState();
  const [data, setData] = useState([]);

  useEffect(() => {
    setLoading(true);
    setError(undefined);
    fetch(~~~)
      .then((res) => res.json())
      .then((data) => {
        setData(data);
      })
      .catch((e) => setError('에러가 발생했습니다.'))
      .finally(() => setLoading(false));
  }, []);
  return [loading, error, data];
}

라는 함수를 만들어, 컴포넌트 마다 const {loading, error, data} = useGetApi() 를 사용해서 뷰와 데이터 패칭을 분리하여 좀더 각각의 관심사에 집중할 수 있습니다

결론은 반복되는 훅 활용 메소드들을 하나로 줄여줌으로써 더 간결하고 보기 좋은 코드를 만들 수 있고, 컴포넌트 로직을 뽑아내서 정의한 후 간편하게 호출할 수 있다는 것이 custom hooks의 장점이 입니다.

Copy link

@JeongBin0227 JeongBin0227 left a comment

Choose a reason for hiding this comment

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

안녕하세요 윤생!
확실히 코드가 많이 깔끔해지고 읽기 좋아진거 같아요! 🎊

충분히 바로 Approve를 드릴 수 있었지만, 질문해주신 부분이나, 추가적으로 말씀드리고 싶은 내용들을 코멘트로 남겨서 해당 부분 확인해보시면 좋을거 같아서 Request changes 로 드립니다~

코멘트 확인부탁드립니다~ step2 하시느라 고생많으셨습니다 👍 💯

<button
type="button"
className="button button--secondary text-caption"
onClick={() => onToggleDrawer()}

Choose a reason for hiding this comment

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

onToggleDrawer 외에 다른 이벤트가 없다면 굳이 안 감싸줘도 될거 같아요!

Comment on lines +31 to +34
const onToggleDrawer = (id: number = NO_SELECT_ID): void => {
setDrawerSelectId(id);
setIsOpenDrawer(!isOpenDrawer);
};

Choose a reason for hiding this comment

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

이 부분과 const [isOpenDrawer, setIsOpenDrawer] = useState<boolean>(false); 해당 상태는 App 에서가 아닌 RestaurantDetailDrawer 안에서 정의해도 되지않을까요? 최대한 App은 가벼운게 좋습니당!

Copy link
Author

@2yunseong 2yunseong Apr 17, 2023

Choose a reason for hiding this comment

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

Drawer를 열고 닫는 부분이 같은 계층에 있는 RestraurantItem과 RestaurantDetailDrawer 둘다에 있어서 다음과 같이 주었습니다!
state 끌어올리기를 참고했어요!!

Choose a reason for hiding this comment

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

아하 RestraurantItem, RestaurantDetailDrawer 둘다 필요하군요! 이해했습니다

);
}
}
const MainHeader: React.FC = () => {

Choose a reason for hiding this comment

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

타입으로 React.FC 를 주는건 좋지 않습니다!
이유는 크게는 defaultProps 를 쓰지 못하는 이유가 있는데, 관련해서 읽을만한 것들 첨부해드리겠습니다 :)
https://yceffort.kr/2022/03/dont-use-react-fc
https://velog.io/@yena1025/FunctionComponent-FC-%EC%82%AC%EC%9A%A9-%EC%A4%84%EC%9D%B4%EA%B8%B0-24jhm0wp

Copy link
Author

Choose a reason for hiding this comment

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

헉 읽을 것 까지.. 감사합니다 케빈 🙇

Comment on lines 6 to 9
type RestaurantProps = {
readonly restaurant: Omit<Restaurant, 'link'>;
restaurant: Omit<Restaurant, 'link'>;
onToggleDrawer: (id?: number) => void;
};

Choose a reason for hiding this comment

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

그런 이유라면 같은 파일에 둬도 좋을거 같아요!
하지만 저는 타입이 general하게 사용되는 타입이 아니고 해당 컴포넌트의 Props에서만 사용하기 때문이라면 같은 파일보단 같은 파일에 두지 않고, 뷰에 집중하기 위해 ~~~.type.tsx 이렇게 파일을 만들어서 나누어서 사용할 거 같아요! 윤생이 말한 방법은 ide에서 타입을 추론해주기 때문에 마우스 커서만 올려두면 알려줄것 같습니다!

Comment on lines 13 to 18
const filterByCategory = (restaurantList, category): Restaurant[] => {
if (category === '전체') return restaurantList;
return restaurantList.filter(
(restaurant) => restaurant.category === category
);
};

Choose a reason for hiding this comment

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

저도 if (category === '전체') 이런 조건이 있으니까 해당 컴포넌트에서만 사용하는 로직이 분명해서 해당 파일 근처에 두는게 좋을거 같은데, 지금 hook 을 배우고있으니까, hook 을 활용해서 관리해보면 어떨까요?

Comment on lines 13 to 18
const filterByCategory = (restaurantList, category): Restaurant[] => {
if (category === '전체') return restaurantList;
return restaurantList.filter(
(restaurant) => restaurant.category === category
);
};

Choose a reason for hiding this comment

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

너무 잘 알고 계시겠지만, 활용할수 있는 힌트는 메인 코멘트에 적어 놓을게요~

@@ -44,7 +44,7 @@ export const SORTING_OPTIONS: SelectOption<Sorting>[] = [
textContent: '이름순',

Choose a reason for hiding this comment

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

이부부 놓쳤는데 CATEGORY_OPTIONS 변수에 textContent 가 option 의 글자로 활용되니까 label 은 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

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

오 좋은 네이밍 조언 감사합니다!!

Copy link
Author

@2yunseong 2yunseong left a comment

Choose a reason for hiding this comment

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

케빈 피드백 반영하고 몇 가지 간단한 질문남겼어요!!

onToggleDrawer: (id?: number) => void;
};

export type SelectContainerProps = Pick<SelectProps, 'onChangeFilterOptions'>;
Copy link
Author

Choose a reason for hiding this comment

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

props 타입을 유틸리티로 해도 괜찮을까요?

괜찮다고 생각한 이유는 위의 SelectProps에 이미 선언되어있으니, 재사용하고 싶어서 그랬습니다.

그런데 지금 작성하고 보니 또 이러면 Props가 추가 되었을 때 확장이 어렵다는 생각이 드는데.. 또 그러면 Union을 사용해 확장하면 되는거 아닌가 싶네요

케빈의 의견도 궁금합니다!

Choose a reason for hiding this comment

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

네 저도 재사용이 가능한 타입은 재사용하는게 좋아보입니다!
만약 추가가 되었을 경우에 Union 을 사용해서 수정하면 될거같아요 👍

Comment on lines +24 to +40
const useFilterRestaurantList = (category, sorting) => {
const handleRestaurant = (restaurantList) => {
const result = pipe(filterByCategory, filterBySort)(restaurantList, [
category,
sorting,
]);
return result;
};

const filteredRestaurantList = useRestaurantList(
'restaurantList',
[],
handleRestaurant
);

return filteredRestaurantList;
};
Copy link
Author

Choose a reason for hiding this comment

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

Hook으로 분리한 로직인데, 필터 처리 useRestaurantList에 핸들러를 넘겨주어 필터링을 처리할 수 있도록 하였습니다.

Copy link

@JeongBin0227 JeongBin0227 left a comment

Choose a reason for hiding this comment

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

안녕하세요 윤생~
미션 수고 많으셨습니다😀 몇가지 코멘트 확인해주세요!
이제 충분히 머지될수 있는 코드라고 생각해서 이만 머지할게요~

처음 다루는 리액트와 커스텀 훅 개념 때문에 많은 러닝 커브가 있었을텐데 그동안 너무 고생많으셨어요 👍

Comment on lines +31 to +34
const onToggleDrawer = (id: number = NO_SELECT_ID): void => {
setDrawerSelectId(id);
setIsOpenDrawer(!isOpenDrawer);
};

Choose a reason for hiding this comment

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

아하 RestraurantItem, RestaurantDetailDrawer 둘다 필요하군요! 이해했습니다

onToggleDrawer: (id?: number) => void;
};

export type SelectContainerProps = Pick<SelectProps, 'onChangeFilterOptions'>;

Choose a reason for hiding this comment

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

네 저도 재사용이 가능한 타입은 재사용하는게 좋아보입니다!
만약 추가가 되었을 경우에 Union 을 사용해서 수정하면 될거같아요 👍

@JeongBin0227 JeongBin0227 merged commit d7832f3 into woowacourse:2yunseong Apr 18, 2023
Copy link

@jiwonh423 jiwonh423 left a comment

Choose a reason for hiding this comment

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

이미 커스텀 훅을 사용해서 "리액트 컴포넌트 자체는 UI와 이벤트만 알게 하자"를 완벽히 지킨것 같네요 🎉 배우고 갑니다~

onToggleDrawer,
}: RestaurantDetailDrawerProps) => {
const restaurantList = useRestaurantList('restaurantList', []);
const restaurant = getRestaurantById(restaurantList, restaurantId);

Choose a reason for hiding this comment

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

"리액트 컴포넌트 자체는 UI와 이벤트만 알게 하자"의 모범답안이네요 👍

let data;
try {
data = JSON.parse(rawData);
} catch (err) {

Choose a reason for hiding this comment

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

에러처리까지 꼼꼼하게 잘 구현하셨네요 😄

import useLocalStorage from './useLocalStorage.ts';
import { identity } from '../util/util.ts';

const useRestaurantList = (key, initValue, callback = identity) => {

Choose a reason for hiding this comment

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

callback의 기본 매개변수를 주니 범용성이 더 좋아진것 같네요 👍

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

Successfully merging this pull request may close these issues.

3 participants