https://brunch.co.kr/@susubaa/72

'Board > 좋은 글' 카테고리의 다른 글

Saying "NO" by Uncle Bob  (0) 2022.11.02

04-automatic-static-optimization

원본 저장소:

next.js/docs/03-pages/01-building-your-application/02-rendering/04-automatic-static-optimization.mdx at canary · vercel/next.js

Next.js kr 저장소: https://github.com/Nextjs-kr/Nextjs.kr

내 저장소: https://github.com/jlee0505/Nextjs.kr

참고 저장소들:

https://github.com/haileyport/Nextjs.kr

'Extracurricular > 활동' 카테고리의 다른 글

새싹톤 우수상 회고  (0) 2024.03.25
[FECrash] 함수형 프로그래밍 스터디 중간 회고  (0) 2023.01.08

https://velog.io/@jlee0505/goorm-x-SeSAC-x-%EC%84%9C%EC%9A%B8%EA%B2%BD%EC%A0%9C%EC%A7%84%ED%9D%A5%EC%9B%90-%EC%83%88%EC%8B%B9%ED%86%A4-%EC%9A%B0%EC%88%98%EC%83%81-%ED%9B%84%EA%B8%B0-%EC%96%B4%EC%84%9C%EC%99%80-%ED%95%B4%EC%BB%A4%ED%86%A4%EC%9D%80-%EC%B2%98%EC%9D%8C%EC%9D%B4%EC%A7%80

문제 마주하기

5/23/23 Tue

문제 상황

  • 지도 생성 기능 앱을 만드려고 함.
  • 지도 생성, 마크 표시 등 모든 기능들이 현재 유저 위치를 받는 것으로 부터 시작함.
  • but, 현재 유저 위치를 받는 getCurrentPosition 내장 함수는 “비동기적”으로 수행됨.
  • 모든 코드를 App 파일에 순차적으로 때려넣었을 때는 순서대로 진행됐지만, 훅으로 분리하니 getCurrentPosition 함수가 값을 받아오는 동안 벌써 다른 함수들이 실행되는 문제가 발생함.
  • 당연히 값이 없으니 undefined 값으로 실행되고 에러가 남.

고민점

  • 어떻게 훅으로 분리할 때 비동기함수를 동기적으로 처리할 수 있을까?

접근

1: useMap 안의 setState
But, 초기값 { lat: 0, lon: 0 } 이 맨처음에 렌더링 되는 게 지저분해보임. 비동기 함수를 좀 더 깔끔히 다루는 방법이 있을 것 같음.

// useMap.tsx

const useMap = () => {
  const [currentPosition, setCurrentPosition] = useState({ lat: 0, lon: 0 });

  const getUserPosition = async () => {
    if (!navigator.geolocation)
      throw new Error("위치 접근 권한을 허용해주세요");

    navigator.geolocation.getCurrentPosition((position) => {
	    const lat = position.coords.latitude;
	    const lon = position.coords.longitude;
	
	    setCurrentPosition({ lat, lon });
	  });
  };

  //...
}
// App.tsx
function App() {
  const { currentPosition, getUserPosition, createMap } = useMap();

  if (Object.keys(currentPosition).length !== 0) {
    createMap(currentPosition.lat, currentPosition.lon);
  }

  useEffect(() => {
    getUserPosition();
  }, []);

  return (
    <div id="app">
      <div id="map" className="map-container"></div>
    </div>
  );
}

 

2. promise, async/await → return new Promise를 통해 해결.

const getUserPosition = () => {
    if (!navigator.geolocation)
      throw new Error("위치 접근 권한을 허용해주세요");

    return new Promise((resolve, reject) => {
      navigator.geolocation.getCurrentPosition((position) => {
        const lat = position.coords.latitude;
        const lon = position.coords.longitude;

        resolve({ lat, lon });
      });
    });
  };
function App() {
  const { getUserPosition, createMap, displayMarker } = useMap();

  useEffect(() => {
    getUserPosition().then((res) => {
      const { lat, lon } = res;
      const map = createMap(lat, lon);
      displayMarker(lat, lon, "내 위치", map);
    });
  }, []);

  return (
    <div id="app">
      <div id="map" className="map-container"></div>
    </div>
  );
}

 


useHttp 훅 만들기

5/24/23 Wed

 

추가적으로, 비동기함수 중 fetch처럼 상태, 에러 등을 관리할 필요가 있다면 상태관리를 위해 훅으로 빼보자.

찾아보니 그 훅은 비슷비슷한 것 같다.

  1. data, status(loading), error : 이 3가지 상태를 관리한다.
  2. useCallback과 useReducer, 그리고 dispatch를 사용한다.

상태를 다루는 방법에는 알다시피 2가지가 있다: useState, useReducer 가 그것이다. 그런데 useHttp훅은 알다시피 다뤄야할 상태가 3가지이다. 따라서 복수 상태를 다루기 더 편리한 useReducer를 사용하는 것이 편할 것이다.(물론 useState를 사용할 수 도 있다.)

2가지 방법으로 모두 훅을 만들어 사용해 보았다.

 

1. useState를 이용한 커스텀훅

// useFetchMoviesUsingUseState.tsx

import { useCallback, useEffect, useState } from "react";

const useFetchMoviesUsingUseState = (url: string) => {
  const [movies, setMovies] = useState([]);
  const [loading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  const fetchMoviesHandler = useCallback(async () => {
    setIsLoading(true);
    setError(null);

    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error("Something went wrong!");
      }

      const data = await response.json();

      const transformedMovies = data.results.map((movieData: any) => {
        return {
          id: movieData.episode_id,
          title: movieData.title,
          openingText: movieData.opening_crawl,
          releaseDate: movieData.release_data,
        };
      });
      setMovies(transformedMovies);
    } catch (error: any) {
      setError(error.message);
    }

    setIsLoading(false);
  }, []);

  useEffect(() => {
    fetchMoviesHandler();
  }, [fetchMoviesHandler]);

  return { movies, loading, error };
};

export default useFetchMoviesUsingUseState;

2. useReducer를 이용한 커스텀훅

// useFetchMoviesUsingUseReducer.tsx

import { useCallback, useReducer } from "react";

const reducer = (state, action) => {
  switch (action.type) {
    case "SEND":
      return {
        data: undefined,
        error: null,
        status: "pending",
      };
    case "SUCCESS":
      return {
        data: action.payload,
        error: null,
        status: "completed",
      };
    case "ERROR":
      return {
        data: undefined,
        error: action.errorMessage,
        status: "completed",
      };
    default:
      return state;
  }
};
const initialState = {
  data: undefined,
  error: null,
  status: "ready",
};

function useFetchMoviesUsingUseReducer(requestFunction) {
  const [state, dispatch] = useReducer(reducer, initialState);
  const loading = state.status === "pending";

  const sendRequest = useCallback(
    async (...requestData) => {
      dispatch({ type: "SEND" });
      try {
        const responseData = await requestFunction(...requestData);
        dispatch({ type: "SUCCESS", payload: responseData });
        return responseData;
      } catch (error) {
        dispatch({ type: "ERROR", errorMessage: error.message });
        return;
      }
    },
    [requestFunction]
  );

  return {
    sendRequest,
    loading,
    ...state,
  };
}

export default useFetchMoviesUsingUseReducer;

확실히 여러가지 상태를 관리할 때에는useState보단 useReducer를 사용하는 편이 가독성이 올라간다고 생각은 하는데, 여전히 작성할 때 코드가 늘어난다는 점에서 (reducer를 따로 작성해줘야 했다) 더 편리한지는 모르겠더라.

 

🔖 학습 참조글
https://itchallenger.tistory.com/258
https://velog.io/@sae1013/Reactcustom-hook으로-httpRequest-구현하기
https://www.udemy.com/course/react-the-complete-guide-incl-redux/learn/lecture/25599794#overview

react-query 이용하기

5/27/23 Sat

 

이렇게 서버측 데이터를 불러오고 관리하는 커스텀훅을 만들다보니 하나 알게된 사실: 한 라이브러리를 쓰면 이렇게 커스텀훅을 만들지 않아도 이러한 훅을 쓴 것처럼 data, isLoading 등 상태관리를 쉽게 할 수 있게 된 것을 알게됐다. 그 라이브러리의 이름은 react-query. 그렇게 react-query에 대해서도 공부해보게 되었다.

 

react-query란?

여러 종류의 데이터(state 등) 중에서도 서버에서 받아온 데이터 상태의 관리를 도와주는 라이브러리이다. 이 글은 react-query를 상세히 다루기 위한 글이 아니어서 이에 대한 학습 및 설명글은 따로 뺐다. 아주 간단히 말하자면  처음 사용시 queryProvider로 감싸주기만 하면 그 하위에선 커스텀훅을 사용했던 것처럼 useQuery훅을 통해 어디서든 서버측 데이터와 데이터의 상태를 이용할 수 있었다. 이렇게 위에서 작성했던 모든 커스텀hook과 api함수들이 불필요해지는 편리함을 맛보았다.

 

react-query 사용시 장점

기대했던대로 수많은 보일러플레이트를 없앨 수 있을 뿐 아니라 staleTime, cacheTime등을 활용해 캐싱 기능을 더 수월하게 다룰 수 있다는 점이 인상깊었다. 또 혼자 공부하다보니 놓쳤는데 커스텀훅으로 상태관리를 하면 말그대로 ‘커스텀’이기 때문에 개발자마다 다르게 작성된다. 하지만 react-query를 사용하면 통일된 방식으로 서버측 데이터 관리가 가능해진다. 이렇게 react-query를 공부하고 사용하며 느낀 장점을 정리하면 아래와 같다:

  • 귀찮은 boilerplate x — isLoading 등 상태, 캐싱 기능
  • 팀원들끼리 통일된 방식으로 상태 관리 가능 — 누구는 isLoading, 누구는 pending 변수명을 다르게 사용하는 것을 막을 수 있다.
// index.tsx

const queryClient = new QueryClient();
console.log(queryClient);
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>
);
// App.tsx

const { data, status, error } = useQuery(["movies"], fetchMovies, {
    refetchOnWindowFocus: false,
    retry: 0,
    staleTime: 5000,
    cacheTime: Infinity,
    onSuccess: (data) => console.log(data),
    onError: (e) => console.log(e.message),
  });

  let content = <p>No Found Movies.</p>;

  if (status === "success") {
    content = (
      <div>
        {data.results.map((movie, idx) => (
          <div key={idx}>{movie.title}</div>
        ))}
      </div>
    );
  }

  if (status === "loading") {
    content = <p>Loading...</p>;
  }

  if (status === "error") {
    content = <p>{error.message}</p>;
  }

  return <div>{content}</div>;
}

 

🔖 학습 참조글
https://tech.kakaopay.com/post/react-query-1/
https://tanstack.com/query/latest/docs/react/overview?from=reactQueryV3&original=https://tanstack.com/query/v3/docs/overview
https://velog.io/@jay/10-minute-react-query-concept#:~:text=리엑트 쿼리는 데이터,데이터를 다시 불러온다
https://2ham-s.tistory.com/407
https://kyounghwan01.github.io/blog/React/react-query/basic/
https://www.youtube.com/watch?v=novnyCaa7To

 


Sum Up

어찌어찌 하다보니 비동기함수로 시작해, 비동기 함수중에서도 fetch를 집중적으로 다루게 되었다. 나아가 fetch해온 데이터(서버측 데이터)를 프론트측에서 리액트로 관리하는 방법에 대해 찾아보게 된 시간이었다.

 

네이티브(useState/useReducer) 그리고 툴(react-query)

useState+useEffect 혹은 useReducer+useCallback를 사용해 관리할 수 있다.(이경우 훅을 따로 분리해 관리하면 깔끔하다.) 그러나 이 경우 수많은 보일러 플레이트가 필요하고, 특히 캐싱에 관련해서도 수많은 작업을 진행해줘야 한다. 또한 규명된 상태관리 폼이 존재하지 않아 팀원들끼리 공통된 상태관리가 힘들 수 있다.(다른 상태명, 로직….) 이러한 점을 보다 쉽게 해주는 것이 라이브러리겠지. redux의 saga 미들웨어와 mobx의 __을 이용할 수 있겠지만, 이들은 서버상태 전용 라이브러리가 아니다. 서버 상태 전용 라이브러리 react-query는 보일러 플레이트를 줄이며, 캐싱, 클라이언트 측과 서버 측 상태의 동기화에 특화된 인터페이스를 제공해 서버측에서 넘어온 데이터 관리를 간단하게 한다. 또한 공통된 서버 상태 관리로 팀작업을 원활히 한다.

 

마무리하며&다음에는...

fetch를 가지고 작업하다 서버측 데이터 상태관리까지 넘어가게 되었는데 많은 사람들이 fetch 대신 axios를 사용하고 있었다. fetch 에서 넘어오는 데이터의 에러 처리를 간단하게 해준다는 장점이 있어서 쓴다고 하는데, 한 번 가볍게 다뤄보는 것도 좋겠다.

목차

1. Redux 선택 이유

2. Redux와 contextAPI 구조적 차이

3. Redux 사용 후기(contextAPI와의 비교)

4. 마무리


1. Redux 선택 이유

아직 후기를 쓰진 못했지만 지난 미션에서는 상태관리로 Context API를 사용했었다. Context API를 사용하며 처음 전역상태관리의 개념, Context API와 다른 전역상태관리 라이브러리의 차이점에 대해 공부했었는데, 이 과정에서 전역상태관리 개념의 최초에 redux가 있다는 걸 알게 되었다. 

 

redux에서 처음 나왔다고 알려져있는 reducer와 dispatch의 개념은 언제나 헷갈리는 개념이었다. 또 React에서 상태를 관리하는 방법에는, 요즘 뜨는 recoil, 네이티브 contextAPI, 기타 mobx... 정말 다양하다고 알려져 있지만, 앞으로 어떤 회사에 가서 어떤 상태관리 라이브러리를 사용하게 되든 근본인 redux를 배워두면 컨셉을 이해하는데 유용하지 않을까 생각했다. 

 

그래서 사실 이번 미션의 필수 요구 사항은 오직 React(contextAPI)로만 상태관리를 하는 것이었음에도 불구하고  redux 를 택하게 되었다. 반항심리는 아니었고 근-본은 과연 어떤지, 네이티브 전역상태관리 기능인 contextAPI와는 어떤 차이가 있는지 직접 써보며 확인하고 싶었다.

 

2.  Redux와 ContextAPI의 차이

아래는 한 블로그에서 하나의 도표로 정리한 Redux vs ContextAPI의 차이점으로 분석하며 내 경험과 비교해보았다.(🔗 Redux vs Context API: When to use them, Tapajyoti Bose)

  Redux  Context API
1. 설치 추가 설치 필요, 최종 번들 크기 증가 React와 함께 제공되는 기본 제공 도구
2. 설정 React 애플리케이션과 통합하려면 광범위한 설정이 필요 최소한의 설정 필요
3. 데이터 정적 데이터동적 데이터 모두에 적합 자주 새로 고치거나 업데이트하지 않는 정적 데이터를 위해 특별히 설계
4. 확장성 초기 설정 후 새로운 데이터/액션을 쉽게 추가할 수 있어 쉽게 확장 가능 새로운 컨텍스트를 추가하려면 처음부터 새로 만들어야 함
5. 디버깅 개발 도구에서도 컴포넌트 구조 디버깅을 쉽게 해주는 놀랍도록 강력한 Redux 개발 도구  고도로 중첩된 React에서는 디버깅이 어려울 수 있음.
6. 코드 구성 UI 로직과 상태 관리 로직을 분리하여 코드 구성 개선 UI 로직과 상태 관리 로직이 동일한 구성 요소에 있음

1. 설치

Redux는 npm i redux redux-react @reduxjs/toolkit  으로 redux포함 3가지 라이브러리를 설치해야한다. 이는 최종 프로그램 번들 사이즈의 향상으로 이어진다. 반면 Context API는 다른 설치 없이 사용 가능하다.

 

2. 설정

Redux는 reducer, slice, store, Provider로 감싸기 등 프로그램 사용을 위해 초기 설정해줘야하는 게 많다. 반면 Context API는 context생성 Provider로 감싸주기로 초기 설정이 간단하다.

 

3. 데이터

Redux는 정적 및 동적 데이터 모두에 적합하다고 하는데, 그 이유는 Redux가 불변성을 유지하고 예측 가능한 방식으로 상태를 관리하도록 설계되었기 때문이다(Thinking in Redux, redux). 

 

반면, Context API는 정적 데이터를 위해 것으로 예를 들면, 테마나 인증 정보, 사용자 언어와 같은 수준의 잘 변하지 않는 데이터를 전역적으로 공유할 때 유용하다고 한다.(when to use context, react) 따라서 Redux와 달리 복잡한 상태관리를 위한 툴은 아니라고 할 수 있겠다.  

 

4. 확장성

Redux는 새로운 데이터나 액션을 추가할 때, 서브store 개념인 slice나 새로운 액션인 reducer를 새로 추가하면 되는 반면, context는 서브context 개념이 따로 없어 수정이 어렵다는 말로 들린다. 

 

하지만  Context API가 보통 가장 상단이 되는 컴포넌트를 감싸는 식으로 사용되는 것과 달리, 컴포넌트와 상태를 별도의 파일로 분리해 보다 확장성 있게 사용하는 방법도 있다고 한다(How to use context effectively, Kent C. Dodds) 따라서 새로운 데이터나 액션을 추가하기 위해 컨텍스트를 처음부터 새로 짜야만한다는 이 부분에 대해서는 의구심이 든다. 

 

5. 디버깅

Redux에서는 리덕스 개발자 도구를 처음 개발하고 배포한 개발자 중 한 명인 Zalmoxisus라는 개발자가 만든 크롬 확장 Redux DevTools이 존재한다. 아래와 같은 디버깅을 위한 다양한 기능을 제공하고, 이를 이용해 훨씬 쉽게 디버깅을 할 수 있다:

  • 시간여행(time-travel) 기능: 과거 상태로 돌아가서 상태 변경 과정을 디버깅할 수 있습니다.
  • 상태 스냅샷 기능: 현재 상태를 스냅샷으로 저장하여 나중에 다시 볼 수 있습니다.
  • 액션 필터링 기능: 특정 액션만 필터링하여 디버깅할 수 있습니다.

반면 Context API는 Provider Hell을 야기할 수 있고(🔗 Context API가 존재하지만 여전히 사람들이 redux와 전역 상태관리 라이브러리를 쓰는 이유, nanalog) 이러한 고도의 중첩의 경우 디버깅이 상당히 복잡하고 짜증나는 작업이 될 수 있다.

 

6. 코드 구성

Redux는 UI 로직과 상태 관리 로직을 분리하여 보다 클린한 코드 구성이 가능하하지만, context API는 UI를 업데이트하는 로직과 상태 관리 로직이 동일한 구성 요소에 있다고 한다. 

 

하지만 이 역시 context에서 reducer 함수나 custom hook의 사용을 통해 충분히 분리할 수 있을 거라 생각해 공감이 가진 않는다.

// 1. useReducer 사용해 UI로직과 상태관리 로직 분리하기

import React, { useReducer } from "react";

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

export const CounterContext = React.createContext();

function CounterProvider(props) {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <CounterContext.Provider value={{ state, dispatch }}>
      {props.children}
    </CounterContext.Provider>
  );
}

export default CounterProvider;
// 2. custom hook(useCounter) 사용해 UI로직과 상태관리 로직 분리하기

import React, { createContext, useContext, useState } from "react";

const CounterContext = createContext();

function CounterProvider(props) {
  const [count, setCount] = useState(0);

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);

  const value = { count, increment, decrement };

  return <CounterContext.Provider value={value} {...props} />;
}

function useCounter() {
  const context = useContext(CounterContext);
  if (context === undefined) {
    throw new Error("useCounter must be used within a CounterProvider");
  }
  return context;
}

export { CounterProvider, useCounter };
// 이렇게 하면, useCounter hook을 사용하여 UI를 업데이트하는 로직과 상태 관리 로직을 분리할 수 있다.

import React from "react";
import { CounterProvider, useCounter } from "./CounterContext";

function Counter() {
  const { count, increment, decrement } = useCounter();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

function App() {
  return (
    <CounterProvider>
      <Counter />
    </CounterProvider>
  );
}

export default App;

 

3. 마무리하며

 

처음 상태관리 라이브러리로 redux를 직접 사용해보며, 시중에 나와있는 redux vs contextAPI 글을 분석해봤다. 4번과 6번 특징에 대해서는 동의하지 않지만, 그외의 특징에는 동의한다. 특히

 

1. 초기 세팅이 너무 많다는 점에 동의한다.

이를 위해 reduxjs/toolkit이 등장했다고 해도 여전히 contextAPI보다는 초기 설정 너무 많아 번거로움.

 

2. 디버깅이 쉽다는 점에 동의한다.

redux 개발자 도구를 사용해서 전역 관리 상태를 가시적으로 볼 수 있는 점이 너무너무 편리했다. redux 자체보다는 redux 개발자 도구을 쓸 수 있다는 점이 좋았다. 

 

3. 단순한 상태라면 redux를 사용할 필요는 없다.

redux는 초기 설정이 번거롭지만, 이후 데이터 확장성이 좋고 보다 복잡하고 많은 상태를 관리할 수 있다는 장점이 있다고 한다. 하지만 굳이 cartSlice 하나의 상태로도 충분했던 이번 미션에서 ContextAPI로도 충분했으리라는 생각이 든다. 

 

테스트, npm 배포 등 여러 학습 포인트가 있었지만, 당장 contextAPI도 이번에 처음 다뤄봤기에 욕심을 버리고 상태관리를 얻어가는데 집중했다.

  • 전역 상태 관리
    • 여러 페이지 컴포넌트에서 필요한 전역 데이터 상태 관리는 어떻게 하는 것이 좋을까?
      • ContextAPI를 사용했다. 우선 상태관리 라이브러리를 사용할 수 없다는 것이 조건이었기에. 이에 다른 상태관리 라이브러리와 ContextAPI에 대해 비교해보고, 상태관리 라이브러리의 필요성에 대해 학습했다.
      • 다른 라이브러리(Redux, recoil)은 어떨까?
      • 이후 상태 관리 라이브러리와 비교하며, 이에 대한 필요성을 학습해볼 수 있는 기회였습니다.
  • 서버 상태 관리
    • 카드 회사 선택 기능은 서버에서 받아온 데이터를 사용한다(는 가정). 이를 어떻게 관리할지를 고민하는 건 전역 데이터 상태 관리와는 또 달랐다. 덕분에 서버 상태관리 라이브러리인 react-query 에 대해 학습해보는 계기가 되었다.
      • react-query 넌 무엇이냐
  • form 상태 관리
    • controlled vs uncontrolled component
    • form 상태 관리 라이브러리: 위 방식으로 했을 때의 불편점
    • formik vs react-hook-form

Reference

form 상태 관리

[React] Form 상태 다루기 (Formik vs react-hook-form)

 

[React] Form 상태 다루기 (Formik vs react-hook-form)

이 포스트에서는 form 상태관리를 하는 방법중에서 React-hook-form 과 Formik을 다루면서 두 라이브러리를 비교하는 글입니다. Form 상태를 다루기 위해서 어떤 것을 사용하고 선호하시나요? Form 상태를

blog.songc.io

Overview | Formik

 

Overview | Formik

Formik documentation, tutorial, guides, and examples

formik.org

서버 상태 관리: 리액트쿼리

React에서 서버 데이터를 최신으로 관리하기(React Query, SWR) | 카카오엔터테인먼트 FE 기술블로그

 

React에서 서버 데이터를 최신으로 관리하기(React Query, SWR) | 카카오엔터테인먼트 FE 기술블로그

조지영(esme) 무언갈 빠르게 좋아합니다. 그래서 변화가 빠른 FE 개발이 적성에 잘 맞습니다.

fe-developers.kakaoent.com

Overview

 

TanStack Query | React Query, Solid Query, Svelte Query, Vue Query

Powerful asynchronous state management, server-state utilities and data fetching for TS/JS, React, Solid, Svelte and Vue

tanstack.com

 


Summary


이 글은 미션을 수행하며 학습했던 컴포넌트 분리 기준에 대한 내용을 정리한 것입니다. 컴포넌트 분리에 대한 절대적인 기준이 없어 여러 선행자분들의 의견을 참고하고자 아래 3가지 강의를 참고해 공통적인 부분을 거르고, 이후 개인적인 의견을 간단히 남기고자 하였습니다.

 

첫 두 강의의 공통점은, "컴포넌트를 쪼개는 절대적 기준 같은 것은 없다. 하지만 상대적으로 잘 쪼개는 컴포넌트 분리 기준은 존재한다."는 것이었는데요, 두 분 모두 '유지보수를 쉽게 하는'것에 기준을 두고 컴포넌트 분리를 설명하신 것이 인상 깊었습니다. 유지보수를 쉽게 하려면 어떻게 해야할까요? 데이터를 미래에 변할 수 있는 것과 상대적으로 잘 변하지 않을 데이터를 구분하는 것이었습니다.

 

조금 더 구체적으로는, 첫 강의자인 원지혁님은 컴포넌트의 구성을 스타일, 로직, 전역상태, 리모드 데이터 스키마(데이터)로 분리하셨고, 유지보수를 위해 변화가 가장 잘 일어나는 데이터 모델에 따라 컴포넌트 분리를 해주는 것을 추천하셨습니다. 두 번째 강의의 한재엽님은 변하는 것에 앞서 설명된 데이터를 데이터계산과 상호작용으로 나누셨습니다. 결국 둘 다 데이터 관리부분인데 처음부터 제공되는 데이터냐, 사용자의 액션이 있어야 계산되는 데이터냐에 따라 한 번 더 나누신듯하다. 변하는 부분을 데이터, 변하지 않는 부분을 UI로 잡고 분리한다는 점에서 원지혁님과 의견이 같았습니다. 두 강의자분 다 컴포넌트 분리의 기준을 유지보수로 잡고 계시고, 상대적으로 잘 변하는 것, 즉 컴포넌트가 다루는 데이터를 기준으로 컴포넌트를 분리하는 것을 제안하신다는 데에서 동일했습니다.

 

결론적으로, 저도 유지보수를 쉽게하는 컴포넌트 분리 기준으로 데이터 모델을 잡는 것에 설득되었습니다. 이 데이터를 변경가능성이 높은 데이터인지, 그렇지 않은 데이터인지 - 즉, 도메인 관련인지 아닌지로 나누는 방법이 좋다는 것도 이해하였습니다. 더하여 데이터는 훅스, UI관련은 컴포넌트, 상태관리는 store 등으로 나누는 기준에 동의합니다. 앞으로 리액트를 활용한 개발에 있어서 컴포넌트 분리는 이러한 기준으로 하고자 합니다. (하지만 컴포넌트 분리에 절대적인 기준이 없는 만큼 이 글을 보시는 분들의 다양한 의견에도 열려있습니다.)

 


강의

당근마켓, 원지혁 - 컴포넌트, 다시 생각하기


요약

  • 바라보기: React 컴포넌트의존성 - React 컴포넌트를 만드는데 필요한 것들은 어떤 것들이 있을까?
    • 기능적 분류(Type): props 와 hooks
    • 특징적 분류(Feature):
      • 스타일(컴포넌트의 스타일, src/components/Something.css)
      • 로직(UI조작에 필요한 커스텀 로직, src/components/Something.useInfinityScroll.ts),
      • 전역 상태(현재 UI를 표현하기 위해 유저의 액션을 통해 초래된 상태, src/store.ts)
      • 리모트 데이터 스키마(API 서버에서 내려주는 데이터의 모양, https://examples.com/api/v2/articles/json)
    • React 컴포넌트의 숨은 의존성
      • 한 컴포넌트에 정보를 추가한다고 하자. 그 컴포넌트에 새로운 props 를 추가하면 된다.
      • 하지만, 그게 끝이 아니다. 숨은 의존성이 존재한다. 타겟 컴포넌트와 루트 컴포넌트 사이의 props들의 추가 수정이 필연적일 수 밖에 없다. props drilling 없이 따로 store를 둔다해도 페이지 기반 라우팅을 한다면 결국 root 컴포넌트에 의존할 수 밖에 없을 것이다. 즉, 해당 컴포넌트에 새로운 정보(의존성)를 추가하기 위해서는 root 컴포넌트 수정도 필연적이다.
  • 함께 두기(co-locate): 이러한 의존성들을 어떻게 정리할 수 있을까?
    • 원칙1 - 비슷한 관심사라면 가까운 곳에(Keep Locality)
      • 한 컴포넌트에 필요한 스타일과 로직은 함께 두기 쉽다.
      • 전역상태는 여러 컴포넌트가 공유하고 있기 때문에 특정 컴포넌트와 함께 두기 어렵다.
      • 리모트 데이터 스키마 함께두기는 좀 더 복잡하므로 원칙2로 따로 분류해보자.
    • 원칙2 - 데이터를 ID 기반으로 정리하기 (Abstraction by Normalization)
      • 데이터 정규화(Normalization) 이라고도 한다.
        • yarn add mormalizer : 정규화를 도와주는 라이브러리
        • 하지만 여전히 숨은 의존성 문제는 존재한다: 사용 컴포넌트의 상위 컴포넌트에서 id 값을 정확히 받아와야 한다.
      • 글로벌 아이디(Global Id): 모델명을 따로 넘길 필요 없이 ID값만 가지고 특정 데이터를 유일하게 식별할 수 있도록 하는 체계.(상위 컴포넌트에서 ID값을 받아올 필요가 없다.)
        • 예시: 첫번째는 필요한 데이터를 바깥(./store.ts)에서 받아오고 있고, 두번째에서는 GlobalId를 이용해서 id 데이터를 같은 컴포넌트 안에 둘 수 있다! (16:02)
        • 도우미: 전역 객체 식별 (Global Object Identification, GOI): (17:02)
  • 이름 짓기(Naming): 프롭스 네이밍(Props Naming)에 대해서 생각해보자.
    • 원칙3 - 의존한다면 그대로 드러내기(Make Explicit)
      • 프로필 컴포넌트 - User 와 User가 의존하고 있는 Image, 이렇게 크게 2가지 데이터에 의존하고 있다. 2번째는 User와 Image의 의존성까지 보여주는 이름짓기를 활용해 더 직관적으로 의존성을 알 수 있다!
      • 하지만 위와 같이 한 컴포넌트에서 여러 모델의 정보(User 컴포넌트 내에 user 뿐 아니라 image 정보도 받아오고 있다.)를 표현하는 것은 일종의 관심사 분리가 제대로 안되었다는 일종의 신호이기도 하기에 아래와 같이 수정해주면 더욱 좋다.
    • 재사용하기(Reuse): 개발할 때 편리하기 위한 것보다 변경할 때 편리하기 위해 = 유지보수에 편하기 위해!
      • 변화하는 부분을 미리 예측하고 컴포넌트를 나누는게 중요하다.
        • 대부분의 변화는 리모트 데이터 스키마가 변화하는 방향을 따라서 움직인다.
        • 그렇다면 어떻게 변하는 것들과 변하지 않는 것들을 분리할 수 있을까?
    • 원칙4 - 데이터 모델 기준으로 컴포넌트 분리하기, Separating Components by Data model
      • 같은 모델을 의존하는 컴포넌트: 재사용
      • 다른 모델을 의존하는 같은 컴포넌트: 분리
  • 결론(Conclusion)
    • 원칙 요약
      • 원칙1 - 비슷한 관심사라면 가까운 곳에
      • 원칙2 - 데이터를 Id 기반으로 정리하기
      • 원칙3 - 의존한다면 그대로 드러내기
      • 원칙4 - 모델 기준으로 컴포넌트 분리하기
    • 추가
      • 이러한 위 4가지 원칙을 강제해서 리액트 클라이언트 개발을 더 편하게 해주는 GraphQL 데이터 레이어 프레임워크 Relay 를 소개해주시면서 강의를 마치셨다.

강의

토스슬래시, 한재엽: Effective Component 지속 가능한 성장과 컴포넌트


요약

  • 무엇이 변경될지 알았다면… 변경은 예측불가하다. 그래서 변경은 예측하지 말고 대응해야 한다.
  • 변경에 대응하기: 변경에 유연하게 대응하도록 컴포넌트를 나누기
    • 만들다보니 페이지가 커지고, 이 커진 코드를 ‘적당히’ 나누기 → ‘적당히’: 변경에 유연하게 대응하도록
    • 어떻게 하면 변경에 유연하게 대응하도록 컴포넌트를 분리할 수 있을까?
      1. Headless 기반의 추상화하기: 변하는 것 vs 상대적으로 변하지 않는 것
      2. 한 가지 역할만 하기: 또는 한가지 역할만 하는 컴포넌트의 조합으로 구성하기
      3. 도메인 분리하기: 도메인을 포함하는 컴포넌트와 그렇지 않은 컴포넌트 분리하기
  • Headless 기반의 추상화하기
    • 컴포넌트는 크게 3가지 역할을 한다:
      • 데이터 관리: 외부에서 받은 데이터, 상태과 같은 내부 데이터를 어떻게 관리하는지
      • UI 관리: 그러고 이러한 데이터를 어떻게 유저에게 보여줄지(UI)
      • 상호작용: UI를 기반으로 어떻게 사용자와 상호작용할지
    • 데이터와 UI의 분리. 컴포넌트를 구성하는데 필요한 데이터를 계산해야 하는데 이 역할을 use~훅스에 위임한다. 이러한 use~훅스는 UI를 관심사에서 제외하고 오직 데이터를 모듈화 하는데에만 집중할 수 있다.
    • UI와 상호작용 분리: 상호작용 부분도 역시 훅스로 만들어 관리한다.
  • 한 가지 역할만 하기(Composition)
  • 도메인 분리하기
    • UI패턴 공통화
  • 결론: 컴포넌트 분리시 고민해야 하는 것
    • 의도가 무엇인가? 분리하면 어떻게 좋아지는가?
    • 이 컴포넌트의 기능은 무엇인가?: 어떤 데이터를 다루는가?
    • 어떻게 표현되어야 하는가?

강의

당근마켓 원지혁 : 그래서 DECLARATIVE가 뭔데?


Declarative UI 패턴을 통해 자바스크립트를 마치 마크업짜듯 작성할 수 있게 되었다 → HTML, CSS / JavaScript 가 아니라 “관심사(정보)”를 기준으로 분리하게 되었다.

정보는 트리구조가 아니다! (함수형-순서형이 아니다!) 정보는 그래프 형태이다!

 

단일 진실 공급원 Single source of Truth

특정 상태가 변경되었을 때, 다른 부수효과를 최소로 하기 위해 단일 상태로 만들자. → ID 하나만 가지고 찾자. (Normalization) React가 UI에 대한 HOW를 제공한다면, GraphQL은 데이터에 대한 HOW를 제공한다.

 

선언적으로 데이터 요청하기 Declarative Data Fetching

리액트 컴포넌트는 데이터에 의존한다. 리액트는 어떤 정보(WHAT)가 필요한지만 요청한다. GraphQL 같은 라이브러리는 데이터를 어떻게(HOW)를 담당해 알아서 데이터를 처리해 넘겨준다. 결국 **선언적(Declarative)**란 공통된 HOW를 잘 분석해서 꺼내고 이를 통해 내 코드에 WHAT을 잘 뭉치는 과정. 이를 위해 HOW와 WHAT을 잘 가르는 안목이 필요하다.

 

'Programming > 웹 프론트엔드' 카테고리의 다른 글

[React] 비동기함수 Hook으로 다루기  (0) 2023.05.30

문제

https://www.acmicpc.net/problem/9251

 

LCS(Longest Common Subsequence, 최장 공통 부분 수열)문제는 두 수열이 주어졌을 때, 모두의 부분 수열이 되는 수열 중 가장 긴 것을 찾는 문제이다.

 

예를 들어, ACAYKPCAPCAK의 LCS는 ACAK가 된다.

풀이

const fs = require("fs");

// 문제 링크 : https://www.acmicpc.net/problem/9251
// 제출 시 path : /dev/stdin

const input = fs.readFileSync("/dev/stdin").toString().trim().split("\n");

const LCS = (str1, str2) => {
  const m = str1.length;
  const n = str2.length;

  const dp = new Array(m + 1).fill(null).map(() => new Array(n + 1).fill(0));

  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (str1[i - 1] === str2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1;
      } else {
        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
      }
    }
  }

  let result = "";
  let i = m,
    j = n;

  while (i > 0 && j > 0) {
    if (str1[i - 1] === str2[j - 1]) {
      result = str1[i - 1] + result;
      i--;
      j--;
    } else if (dp[i - 1][j] > dp[i][j - 1]) {
      i--;
    } else {
      j--;
    }
  }

  return result;
}

const solution = (input) => {
  const [firstArr, secondArr] = input;
  const answer = LCS(firstArr, secondArr);
  return answer.length;
};

console.log(solution(input));

해설

for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (str1[i - 1] === str2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1;
      } else {
        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
      }
    }
  }

str1 = ACAYKP

str2 = CAPCAK

dp[i][j] j=0 j=1 j=2 j=3 j=4 j=5 j=6
i=0 0 0 0 0 0 0 0
i=1 0 0 1 1 1 1 1
i=2 0 1 1 1 2 2 2
i=3 0 1 2 2 2 3 3
i=4 0 1 2 2 2 3 3
i=5 0 1 2 2 2 3 4
i=6 0 1 2 3 3 3 4

i=0 -> j=0,1,2,3,4,5,6 다 비교. 

 

dp[1][1]:

ACAYKP 과 CAPCAK 비교

A !== C 이므로 

dp[1][1]은dp[0][1]과 dp[1][0] 중 최댓값 

dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) 

 

dp[1][2]:

ACAYKP 과 CAPCAK 비교

A === A 이므로 

dp[1][2]은 dp[0][1]+1 

dp[i][j] = dp[i - 1][j - 1] + 1; 

 

'Board > 알고리즘' 카테고리의 다른 글

백준 1463 자바스크립트  (0) 2023.02.15
백준 9663 N-Queen  (0) 2023.02.08
백준 15652 N과 M(4)  (0) 2023.02.05
백준 15651 N과 M(3)  (0) 2023.02.05
백준 15650 N과 M(2)  (0) 2023.02.05

문제

https://www.acmicpc.net/problem/1463

접근

1을 빼거나 2나 3을 나누어 1로 만드는데 걸리는 연산 횟수의 최솟값을 구하는 문제입니다.

첫째줄는 1이상 10^6이하의 정수 N이 주어집니다. 수의 범위가 크기 때문에 계산가능한 모든 수를 구하는 완전탐색 등은 이 문제에 어울리지 않은 접근법입니다. 한 번 연산한 값을 기억해 두고 문제를 풀 수 있는 DP가 적절해 보입니다.

 

DP의 인덱스는 숫자를 뜻합니다.

D[N-1]: N에서 1을 빼서 1을 만드는 연산 횟수

D[N/2]: N에서 2를 나눠 1을 만드는 연산 횟수

D[N/3]: N에서 3을 나눠 1을 만드는 연산 횟수

 

D[N] 숫자 N이 1이 되는데 걸리는 최소한의 연산 횟수를 저장합니다.

N이 2로 나눠진다면 D[N] = D[N-1] + 1 과 D[N/2] + 1 중 더 작은 값, N이 3으로 나눠진다면 D[N] = D[N-1] + 1 과 D[N/3] + 1 중 더 작은 값이 됩니다.

각 경우의 수 뒤에 +1 이 되는 이유는 D[N-1],D[N/2],D[N/3] 이 각 N을 구하기 이전의 연산횟수를 나타내기 때문입니다.

 

예를 한 번 들어볼까요?

D[N-1] + 1 :

4를 1을 빼서 1로 만드는 연산 횟수는 3입니다. 4-1-1-1=1

3을 1을 빼서 1로 만드는 연산 횟수는 2입니다. 3-1-1=1

2를 1을 빼서 1로 만드는 연산 횟수는 1입니다. 2-1=1

이를 뒤집어보면 숫자 N을 1을 만들 때까지 1을 빼는 연산의 횟수는 N-1을 1을 만들 때까지 1을 빼는 연산의 횟수 +1 임을 알 수 있습니다. 

D[N/2] + 1

2를 2로 나눠서 1로 만드는 연산 횟수는 1입니다. 2/2 = 1

4를 2로 나눠서 1로 만드는 연산 횟수는 2입니다. 4/2 = 2, 2/2 =1 

8을 2로 나눠서 1로 만드는 연산 횟수는 3입니다. 8/2 = 4, 4/2 = 2, 2/2 = 1

이를 뒤집어보면 숫자 N을 1을 만들 때까지 2를 나누는 연산의 횟수는 N-1을 1을 만들 때까지 N을 나누는 연산의 횟수 +1 임을 알 수 있습니다.

D[N/3] + 1도 위와 D[N/2] + 1의 경우와 마찬가지입니다.

 

문제를 한 번 풀어볼까요? N=4 로 예를 들어봅시다.

우선 D[0]은 문제 범위 밖이므로 0으로 처리해 줍니다. 

D[1] = 0 입니다. 1을 1로 만들 수 있는 연산의 횟수는 0회이기 때문입니다.

D[4] = Math.min( D[3] + 1 , D[4/2] +1) = Math.min( 1 + 1, 2 + 1 ) = Math.min(2, 3) = 2 가 됩니다.

 

이를 코드로 풀어보면 아래와 같습니다.

풀이

const input = fs
  .readFileSync("/dev/stdin")
  .toString()
  .trim()
  .split("\n")
  .map((v) => v.split(" ").map((v) => parseInt(v)));
const solution = (input) => {
  const N = +input;
  const DP = Array.from({ length: N + 1 }, () => 0);

  for (let i = 2; i < N + 1; i++) {
    DP[i] = DP[i - 1] + 1; // 1을 빼서 1로 만드는 연산의 횟수로 우선 설정

    if (i % 2 === 0) {
      // 2로 나눠진다면,
      DP[i] = Math.min(DP[i], DP[i / 2] + 1); // 1을 빼서 1로 만드는 연산의 횟수와  2로 나눠 1로 만드는 횟수 중 더 작은 경우로 선택.
    }

    if (i % 3 === 0) {
      DP[i] = Math.min(DP[i], DP[i / 3] + 1);
    }
  }

  return DP[N];
};

console.log(solution(input)); // 3

 

'Board > 알고리즘' 카테고리의 다른 글

백준 9251 LCS [JavaScript / Node.js]  (0) 2023.02.23
백준 9663 N-Queen  (0) 2023.02.08
백준 15652 N과 M(4)  (0) 2023.02.05
백준 15651 N과 M(3)  (0) 2023.02.05
백준 15650 N과 M(2)  (0) 2023.02.05

[NEXTSTEP] TDD with React

 

들어가며

 NEXTSTEP 에서 진행하는 TDD with React 2기 프로그램에 운이 좋게 참가하게 되었다. 

 잘 모르시는 분들을 위해 간단한 설명을 덧붙이자면, NEXTSTEP은 개발 교육 프로그램으로, 우아한형제들 개발자분들이 연사로 참여하신다. 내가 참여한 이번 TDD with React 코스는 2월 6일(월) ~ 3월 28일(화)까지 총 8주간 진행되는교육프로그램으로, TDD(Test-Driven Development)와 CDD(Component-Driven Development) 등 React로 클린코드 개발 학습을 목표로 한다.

 

 이번주는 온보딩 미션 기간이었다. 이번 글에서는 온보딩 미션으로 리액트로 계산기 구현 미션을 진행하며 배운 것을 정리해보고자 한다. 

 

+ 추가로 파이널 미션이 있다.

 

미션을 진행하며 내가 배운 것들 - React 관련

이번 미션의 리뷰어님은 오태은님이셨다. 리뷰어님이 남기신 코멘트에서 다시 찾아봐야겠다 싶은 부분 중 리액트 관련 내용만 추려봤다:

1) props 에서 익명함수 가능한 자제하기

2) 컴포넌트 분리 (a.k.a 선언적이다의 뜻)

 

1)  props 에서 익명함수 가능한 자제하기

1-1) 불필요한 레퍼런스 변경 막기 

 리액트에서 이벤트를 핸들링할 때 인자가 필요한 콜백함수를 넘겨주어야하는 경우가 있다. 콜백함수를 인자를 넘겨주는 방법에는 익명함수, 이벤트 객체 사용으로 2가지가 있지만, 그중 익명함수를 쓰는 것이 대표적이라고 한다. 따라서, 인자를 넘겨주는 콜백함수를 쓰는 경우를 대비해 익명함수로 콜백을 넘겨주는 걸 습관으로 들였었다.

 하지만 콜백을 익명함수로 넘길 경우 JSX가 호출될 때마다 새로운 함수를 생성하게 된다고 한다. 즉, 그 때마다 새로운 레퍼런스를 참조하게 되는 것이다. 계속해서 새로운 레퍼런스를 참조하게 되면 vdom을 실제dom에 반영하는 과정이나 useMemo를 사용하는 경우에 좋지 않다고 한다. 따라서 인자를 넘겨주어야 하는 콜백이 아니면 익명함수로 넘기지 않는 것이 성능상 좋다.

 

1-2) 객체 타입은 useMemouseCallback 사용하기

 첨언으로 useMemo와 useCallback에 대해서도 언급해주셨는데, 이부분은 아직 다루지 않은 개념이라 낯설어 찾아봤다. 간단히 말해, useMemo는 한 번 계산한 값을 다시 쓰는 것이고 useCallback은 한 번 사용한 함수 결과를 저장했다 재사용하는 것이다. 

 JSX 호출마다 객체에 대한 참조값도 계속 바뀌게 되는데, 호출마다 새로운 레퍼런스를 참조하는 것은 앞서 언급했듯 앱의 성능을 낮춘다. 따라서, object 나 array 같이 객체 사용에 있어서 useMemo 를 사용하는 것이 좋다.

 

2) 컴포넌트 분리  (a.k.a 선언적이다의 뜻)

리액트를 제대로 공부한지 얼마안됐지만 항상 언급되는 것이 제대로된 '컴포넌트의 분리'인듯 하다. 리액트의 핵심 개념인 컴포넌트를 어떻게 재사용가능하게 분리하는 지가 중요한가보다. 이와 관련해서는 그냥 (1)컴포넌트는 재사용이 가능해야 하고, (2)앱 전반적 비즈니스 로직별로 굵직하게 나누는게 좋다고 알고 있다. 그런데 이걸 의도해서 적용한 건 아니었고...주어진 기본 HTML의 구조가 크게 4개로 나누어지는 것 같아, 그 역할대로 나누었을 뿐이었다. 그런데 '선언적'이라는 용어를 쓰시며 뜻밖의 칭찬을 들었다. 그래서 찾아보게 되었다.

 

2-1) 리액트 컴포넌트 분리는 어떤 기준으로 하는 게 좋을까?

 

 해당 물음에 대한 답을 찾아보다 FEConf2021의 원지혁님 강의와 토스슬래시의 한재엽님의 강의를 보게 되었다. 두 분의 의견에 따르면, "컴포넌트를 쪼개는 절대적 기준 같은 것은 없다. 하지만 상대적으로 잘 쪼개는 컴포넌트 분리 기준은 존재한다."는 것이었다.두 분 모두 '유지보수를 쉽게 하는'것에 기준을 두고 컴포넌트 분리를 설명하신 것이 인상 깊었다. 유지보수를 쉽게 하려면 어떻게 해야할까? 미래에 변할 수 있는 것과 상대적으로 잘 변하지 않을 것 구분하는 것이다.

 

 원지혁님은 컴포넌트의 구성을 스타일, 로직, 전역상태, 리모드 데이터 스키마(데이터)로 분리하셨다. 유지보수를 위해 변화가 가장 잘 일어나는 데이터 모델에 따라 컴포넌트 분리를 해주는 것을 추천하셨다. 한재엽님은 변하는 것에 앞서 설명된 데이터를 데이터계산과 상호작용으로 나누셨다. 결국 둘다 데이터 관리부분인데 처음부터 제공되는 데이터냐, 사용자의 액션이 있어야 계산되는 데이터냐에 따라 한 번 더 나누신듯하다. 변하는 부분을 데이터, 변하지 않는 부분을 UI로 잡고 분리한다는 점에서 원지혁님과 의견이 같았다. 두 분 다 컴포넌트 분리의 기준을 유지보수로 잡고 계시고, 상대적으로 잘 변하는 것, 즉 컴포넌트가 다루는 데이터를 기준으로 컴포넌트를 분리하는 것을 제안하신다는 데에서 동일했다.

 

 이것을 이번 계산기 모델에 적용해보았다. Total, Digits, Modifier, Operations 4가지 컴포넌트가 모두 useCalculator를 통해 calculator 데이터 모델 하나만 필요하다. calculator 모델에는 4개의 메서드 -숫자입력(addDigit), 연산자입력(addOperation), 계산(caculate), 리셋(reset)-상태관리 부분-현재 상태(state)와 현재 상태를 변경하는 calcReducer-가 있다. 각 컴포넌트 Total -> state.total, Digits -> addDigit, Operations -> addOperation, calculate, Modifier -> reset 에 의존하고 있다. 아마 데이터 모델을 calculator 하나에 다 때려박지 않고 각각 나눠서 정의했다고 친다면, 필요한 데이터에 따라 컴포넌트를 잘 나눈 것이기 때문에 칭찬을 받은 게 아닐까? 다만 이번 계산기 미션은 온보딩이고 작은 앱인만큼, 굳이 데이터를 여러개로 나눌 필요가 없다고 생각하셔서 데이터 모델을 분리하는 것까지는 언급하지 않으셨던게 아닐까하는 생각이 든다. 

 

2-2) 리액트 컴포넌트 분리를 '선언적'으로 한다는 것은 어떤 의미일까?

 

"...컴포넌트는 선언적이다: 컴포넌트는 개발자로 하여금 어떻게(HOW)UI를 그려야 할지에 대한 고민 없이 주어진 상태에 기반해서 어(What) UI가 생겨야하는지 적기만 하면 되도록 도와준다. - Thinking in Relay

 의도한 것은 아니었지만 '선언적이다'는 용어의 의미를 좀 더 명료하게 알기 위해 찾아보다보니 원지혁님의 강의가 상단에 떴다.

 

 결국 선언적이란 건 무엇인지에 대해서만 짧게 추리자면, 선언적(Declarative)의 뜻은 누가봐도 명료하도록 컴포넌트의 분리를 나눈 것이다. 반대 개념으로 언급되는 명령형(Impertaive)가  로직의 순서대로 구현하여 흐름을 파악하기 위해 순서대로 코드를 전부 읽어야 한다면, 선언적이라는 것은 그중 공통된 주제(데이터)를 가진 로직(HOW)를 엮어 주제별(WHAT)으로 잘 뭉쳤다는 뜻인 것 같다. 

 

 사실 이렇게 컴포넌트를 나눈 것은 다른 미션수행자의 안목을 빌려온 것인데, 다음 미션부터는 어떻게 데이터를 관리하고 유저와 상호작용할 지에 대한 HOW를 고민해보고, 그 중 공통된 HOW들을 뭉쳐 WHAT 으로 잘 선언하는 설계하는 고민에 시간을 들여야겠다고 생각했다.

 

미션을 진행하며 내가 배운 것들 - React 외의 것들

리액트와 관련된 부분 이외의 자잘한 부분들을 정리하면 아래와 같다.

1) 왜 jsx 는 확장자를 js 로 해도 돌아가나

2) 정규식을 제대로 사용하기

3) 에러핸들링을 통해 사용자 경험 높이기

4) prettier 설정, 정확히 알고 하기:

4-1) 디폴트 설정 굳이 중복하지 말기 4-2) extend: [prettier: recommend] 4-3) VSCode 에서이 설정과 프로젝트별 설정

정규식 공부하자
에러핸들링 공부하자

 

마무리하며

 간단한 미션인데도 복습하고 공부할 게 꽤 많았다.

 

 리액트 문법에 대해서만 배웠던 현재 단계에서, 한 단계 더 들어가 1)리액트 최적화 2)리액트 컴포넌트 분리 등 어떻게 더 '잘' 짤 것인지에 대한 고민과 공부를 할 수 있었던 시간이었다. 이러한 면에서 리액트 '문법만' 다룰 줄 아는 학습 단계에 계신 분들께 감히 추천하고 싶다.

 

 또 각 미션별로 이렇게 회고를 쓰는 것이 학습에 정말 많이 도움이 되었다는 구하지 않은 조언도 덧붙이고 싶다. 회고를 쓰며 '이 미션에서 얻은 것'을 정리하기 위해 리뷰를 학습 방향 지표로 삼아, 추가적으로 공부할 수 있었다. 아니었더라면 단순히 리뷰를 읽고 끝냈을 것이다...

 

 이제 겨우 온보딩일 뿐이지만 배운 것이 많은 한 주였다. 우선 미션을 하고, 그 후 리뷰에 따라 학습하고 수정해나가나는 자세에서 진정한 Learn by Doing 을 느낄 수 있었다. 그냥 Doing 만 하면 학습효과를 이만큼 못가져간다는 걸 회고를 쓰며 깨달았다. 마지막까지도 꾸준히 배우고 회고하는 자세를 가져갈 수 있도록 노력해야겠다. 

 


2023.03.19 Updated

문제

https://www.acmicpc.net/problem/9663

 

9663번: N-Queen

N-Queen 문제는 크기가 N × N인 체스판 위에 퀸 N개를 서로 공격할 수 없게 놓는 문제이다. N이 주어졌을 때, 퀸을 놓는 방법의 수를 구하는 프로그램을 작성하시오.

www.acmicpc.net

접근

2가지 방법이 있다: 

- 각 스퀘어를 전부 탐색하며 조건을 찾는다. => n^n 

- 한 행에 하나의 퀸이 온다는 것을 이용해 행별로 탐색 => n (풀이 참조)

 

2번째의 경우가 훨씬 시간복잡도가 줄기 때문에 두 번째 방법으로 간다

즉, 행이나 열 하나를 잡고 그 행에 첫 퀸을 두었을 때 다음 행이나 열을 돌면서 경우의 수를 탐색한다. 

 

풀이

const input = require("fs").readFileSync("/dev/stdin").toString();

const solution = (input) => {
  const N = +input;
  const row = Array(N).fill(0);
  let cnt = 0;

  const isPossible = (x) => {
    for (let i = 0; i < x; i++) {
      if (row[x] === row[i]) return false;
      if (x - i === Math.abs(row[x] - row[i])) return false;
    }
    return true;
  };

  const dfs = (x) => {
    if (x === N) {
      cnt++;
      return;
    }

    for (let y = 0; y < N; y++) {
      row[x] = y; 
      if (isPossible(x)) {
        dfs(x + 1); 
      }
    }
  };

  dfs(0);

  return cnt;
};

console.log(solution(input));

'Board > 알고리즘' 카테고리의 다른 글

백준 9251 LCS [JavaScript / Node.js]  (0) 2023.02.23
백준 1463 자바스크립트  (0) 2023.02.15
백준 15652 N과 M(4)  (0) 2023.02.05
백준 15651 N과 M(3)  (0) 2023.02.05
백준 15650 N과 M(2)  (0) 2023.02.05

문제

https://www.acmicpc.net/problem/15652

 

15652번: N과 M (4)

한 줄에 하나씩 문제의 조건을 만족하는 수열을 출력한다. 중복되는 수열을 여러 번 출력하면 안되며, 각 수열은 공백으로 구분해서 출력해야 한다. 수열은 사전 순으로 증가하는 순서로 출력해

www.acmicpc.net

접근

자연수 N과 M이 주어졌을 때, 아래 조건을 만족하는 길이가 M인 수열을 모두 구하는 프로그램을 작성하시오.

  • 1부터 N까지 자연수 중에서 M개를 고른 수열
  • 같은 수를 여러 번 골라도 된다.
  • 고른 수열은 비내림차순이어야 한다.

➡️ 어렵게 생각할 것 없이 중복을 허용하는 순서가 상관없는 경우의 수를 구하라는 것이다: 중복순열. 순열 문제였던 백준 15650 N과 N(2) 문제에서 중복체크하는 checked 배열만 없애주면 된다.

풀이

const input = require("fs")
  .readFileSync("/dev/stdin")
  .toString()
  .trim()
  .split(" ")
  .map(Number);

const [N, M] = input;
const output = [];
let result = "";

function dfs(level, start) {
  if (level === M) {
    result += `${output.join(" ")}\n`;
    return;
  }

  for (let i = start; i < N; i++) {
    output.push(i + 1);
    dfs(level + 1, i);
    output.pop();
  }
}

dfs(0, 0);

console.log(result);
 

'Board > 알고리즘' 카테고리의 다른 글

백준 1463 자바스크립트  (0) 2023.02.15
백준 9663 N-Queen  (0) 2023.02.08
백준 15651 N과 M(3)  (0) 2023.02.05
백준 15650 N과 M(2)  (0) 2023.02.05
백준 15649 N과 M(1)  (0) 2023.02.05

+ Recent posts