• cdn을 이용하거나 직접 다운받아 서버에서 공급할 수 있다. 둘다 알아보자

1. 폰트 호스팅 방식(다운로드 후 서버에서 공급)

무료 폰트 다운받기 (google font)

  • 구글폰츠 등에서 쉽게 폰트를 다운받을 수 있으나 woff 등 모던한 파일형식은 다운 받을 수 없음.
  • 구글폰트헬퍼 싸이트 이용 woff 파일로 받기 혹은 직접 변환하기

GlobalStyle 컴포넌트에서 폰트적용

  • GlobalStyles 내부에서 @font-face쿼리를 적용하면 경고문뜨고 폰트가 매번 깜빡임..
  • fonts.css에서 @font-face쿼리이용 하고 GlobalStyles에서 해당 css를 import
  • index.js 에서 import해도 상관은 없지만 사용처에서 import 하는게 깔끔.
// src/styles/fonts.css
@font-face {
  font-family: "open-sans";
  src: url("../assets/fonts/open-sans.woff");
}
@font-face {
  font-family: "noto-sans-kr"; /*여기서 지정한 이름으로 사용*/
  src: url("../assets/fonts/noto-sans-kr.woff");
}
// src/styles/GlobalStyles.js

import "./fonts.css"

export const GlobalStyles = createGlobalStyle`
  body {
    font-family: "open-sans", "noto-sans-kr", "sans-serif";
  }
`;

2.CDN 이용

  • local 방식과 마찬가지로 globalstyles 내부에서 임포트 쿼리사용 할 경우 경고문과함께 비정상작동
  • css 파일을 이용
// src/styles/fonts.css
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700&family=Open+Sans:wght@500;700&display=swap');
// src/styles/GlobalStyles.js

import "./fonts.css"

export const GlobalStyles = createGlobalStyle`
  body {
    font-family: "Noto Sans KR", "Open Sans", "sans-serif"
  }
`;

cdn vs 호스팅

좌) 호스팅, 우) CDN

- 실험해보니 적어도 나의 프로젝트에서는 속도에서 큰 차이가 없다.

- 안정성을 생각해보면 cdn이 더 나을듯(내 서버 보단 google서버가 안정적일테니)

리덕스의 비동기처리

기본적으로 컴포넌트 내부에서 비동기 로직을 처리한 뒤 리덕스 스토어로 값을 보낼 수 있다. but lean 컴포넌트 등의 이유로 로직을 리덕스에 담아두고 싶다면 다른 방법이 필요


thunk 사용하기

createSlice를 사용해 reducers 를 정의 할때 자동적으로 actions 객체를 만들어 준다. 여기서는 비동기처리를 할 수 없다.(redux내부 로직이 이유)

// src/store/auth.js

const authSlice = createSlice({
  name: "auth",
  initialState: initailAuthState,
  reducers: {
    login(state) {
      state.isAuthenticated = true;
    },
    ...
  }
  // authSlice.acions가 자동으로 생성 된다.

자동으로 생성되는 actions 말고 action 직접 생성할 수 있다.
그렇게 되면 해당 action을 정의 할 때 비동기 처리 가능함


// store/cart-actions.js

import { cartAcions } from "./cart-slice";
import { uiActions } from "./ui-slice";

// 비동기 함수를 반환해준다.
export const getCartData = () => {
  const { showNotification } = uiActions; // 에러처리 액션
  const { setItems } = cartAcions; // 데이터 출력 액션

  // 첫번째 인자로 dispatch가 들어오게되어(내부로직) action을 가져와서 dispatch할 사용할 수있다.
  return async (dispatch) => {
    try {
      const res = await axios.get("/data");
      dispatch(setItems(res.data));
    } catch (err) {
      dispatch(
        showNotification({
          status: "error",
          title: "error!",
          message: "error getting cart data",
        })
      );
    }
  };
};

커스텀 액션 사용 in App.js

// App.js
import { getCartData } from "./ui-actions";

function App() {
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(getCartData()); //커스텀 액션을 디스패치
  }, []);

  return <>...</>;
}

반복적으로 처리되는 데이터 처리 로직(pending, fulfilled, rejected 등)을 간편하게 하는 등 thunk를 편하게 쓰기 위해 툴킷에서는 createAsyncThunk라는 api를 지원해준다.


createAsyncThunk 이용하기

위 처럼 직접 생성한 액션에서 로직에 따라 직접 dispatch 하는 것은 경우에 따라 바람직하지 않을 수 있음.

createAsyncThunk를 이용해 데이터를 fetch해오는등 최소한의 비동기 작업만들 분리해놓고 그에따른 후속작업은 개별 슬라이스 extraReducer에 쉽게 작성해 놓을 수 있음.

// store/cart-actions.js

import { createAsyncThunk } from "@reduxjs/toolkit";

const getCartData = createAsyncThunk("GET_CART_DATA", async () => {
  const res = await axios.get("/cart.json");
  return res.data; //리턴값은 바로 payLoad에 담김

  // 핵심로직 및 에러 처리시 개별 담당 슬라이스에 디스패치하는것이아니라
  // 개별 슬라이스 내부에서 미리 정의가능
});
// ...

export { getCartData };
// store/cartSlice.js, 핵심로직처리(get 후 items에 넣기)

import { getCartData } from "./cart-actions";

const initialState = {
  items: [], //...
};
const cartSlice = createSlice({
  name: "cart",
  initialState,
  reducers: {
    //...
  },
  //비동기 액션에 대해 pending, fulfilled, rejected 상태에 대한 후속 작업을 정의할 수 있다.
  extraReducers: (builder) => {
    builder.addCase(getCartData.fulfilled, (state, action) => {
      //createThunk 내부의 함수 리턴값은 actions.payload에 담김
      state.items = action.payload.items;
      //...
    });
  },
});
//...
// store/uiSlice.js   , getCartData의 송신상태표시 로직(ui)

import { getCartData } from "./cart-actions";

const uiSlice = createSlice({
  name: "ui",
  initialState: { notification: null },
  reducers: {
    // ...
  },
  extraReducers: (builder) => {
    builder.addCase(getCartData.pending, (state, action) => {
      state.notification = {
        status: "pending",
        title: "pending...",
        message: "pending sending cart data...",
      };
    });
    builder.addCase(getCartData.fulfilled, (state, action) => {
      state.notification = {
        status: "success",
        title: "success!",
        message: "success sending cart data!",
      };
    });
    builder.addCase(getCartData.rejected, (state, action) => {
      state.notification = {
        status: "error",
        title: "error!",
        message: "error sending cart data",
      };
    });
  },
});
// App.js
import { getCartData } from "./ui-actions";

function App() {
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(getCartData());
    // extra리듀서 정의시 콜백인자의 action.payload에 해당 액션의 리턴값이 들어간다.
    // 만약 여기서  argument를 넣는다면 action.args에 들어가게 된다.
    //일반 커스텀 액션 및 리듀서의 자동 생성 액션과는 다른 createThunk의 특징이라면 특징..
  }, []);

  return <>...</>;
}

리액트 리덕스(툴킷)

리덕스 툴킷은 리덕스의 서트파티 라이브러리로 리덕스에서 운영하며 공식적으로 권장되는 리덕스의 사용 툴이다.

 

0. 패키지 설치

npm i react-redux
npm i @reduxjs/toolkit

redux 자체는 설치필요 x 툴킷에 패키지에 내장되어있음

 

1. 개별 슬라이스 생성

// src/store/auth.js

import { createSlice } from "@reduxjs/toolkit";

const initailAuthState = {
  isAuthenticated: false,
};

// slice는 리덕스 스토어를 여러개로 분리시켜 관리할 수 있게해준다.
// 초기 상태와 actions를 정의한다
// type별  reducers들을 하나로 통합해 생성해준다. 그걸 export해 store/index 에서 스토어 생성에 이용

const authSlice = createSlice({
  name: "auth",
  initialState: initailAuthState,
  reducers: {
    login(state) {
      state.isAuthenticated = true;
    },
    logout(state) {
      state.isAuthenticated = false;
    },

    //두번째 인자로 사용하는곳으로부터 데이터를 받아올 수있다.
    someFn(state, action) {
      console.log(action.payload); //action.payload에 담겨옴
    },
  },
});

// actions는 스토어에 변경을 줄 곳에서 사용
export const authActions = authSlice.actions;
export default authSlice.reducer;

 

2. 스토어 생성

// src/store/index.js

// 사용자가 정의한 개별 slice 들을 임포트 해서 스토어 생성
import { configureStore } from "@reduxjs/toolkit";
import authSliceReducer from "./auth";
import counterSliceReducer from "./counter";

const store = configureStore({
  reducer: { counter: counterSliceReducer, auth: authSliceReducer },
});

export default store;

 

3. 리액트 전역 상태 선언

정의한 스토어를 리액트에 뿌려준다.

// index.js

//import ...
import { Provider } from "react-redux";
import store from "./store";

const root = ReactDOM.createRoot(document.getElementById("root"));

root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

// 이제부터 리액트 프로젝트에서 상태 변경 및 가져오기 가능

 

4. 상태 가져오기

import { useSelector } from "react-redux";
// useSelector 훅 이용 상태 반영
// isAuthenticated는 개별 슬라이스에서 정의된 상태값

const Header = () => {
  const isLoggedIn = useSelector((state) => state.auth.isAuthenticated);
  // ...

  return (
    <header className={classes.header}>
      {/* ... */}
      {isLoggedIn && <button>Logout</button>}
    </header>
  );
};

export default Header;

 

4. 상태 변경하기

import { useDispatch } from "react-redux";
import { authActions } from "../store/auth";
//useDispatch 훅을 이용, 개별slice에서 정의한 actions을 dispatch할 수 있다..

const Auth = () => {
  const dispatch = useDispatch();
  const { login } = authActions;

  const submitHandler = (e) => {
    dispatch(login());
    dispatch(someFn({ data: "data" })); // payload 전송
  };
  return <form onSubmit={(e) => submitHandler(e)}>{/* ... */}</form>;
};

export default Auth;

 

5. 외부 슬라이스 상태의 영향 받는 슬라이스

// src/store/items.js

import authActions from "./auth";

// slice는 리덕스 스토어를 여러개로 분리시켜 관리할 수 있게해준다.
// 초기 상태와 actions를 정의한다
// type별  reducers들을 하나로 통합해 생성해준다. 그걸 export해 store/index 에서 스토어 생성에 이용

const { logout } = authActions;

const itemSlice = createSlice({
  name: "items",
  initialState: { items: [] },
  reducers: {
    // add item...
    // remove item...
  },
  extraReducer: (builder) => {
    //외부리듀서 반응
    builder.addCase(logout, (state, action) => {
      state.items = []; //logout시 아이템 비우기
    });
    // builder.addCase ...
    // builder.addCase ...
  },
});

export default itemSlice.reducer;

 

선요약

전체코드

 

- portal을 이용한 Modal의 fade효과 관련 문제점을 해결해주는 전역위치 모달

- 주의점

  • 모달이 생성된 이후부터는 모달내부에서 상태 업데이트를 받기 어려움(전역상태를 이용하면 극복가능)
  • 모달의 전역 context위치에 유의해야함(전역 상태 중 가장 안쪽으로 두어야 안전)

 

 

포탈을 이용한 Modal에 Fade효과 적용시 문제점

흔히 모달 컴포넌트는 portal을 이용해 root위치에 렌더링 되는 방식으로 구현합니다. 그리고 모달의 fade-in, fade-out 효과는 time-out을 이용해 구현하는 방식이 많이 사용되는듯 합니다.

 

그런데 해당 방식으로 프로젝트의 모달을 구현하는데에 한가지 문제가 발생했습니다.

 

 

(위 코드는 mui의 fade Modal 컴포넌트 입니다. Portal을 이용한 Modal 컴포넌트와 유사하게 작동해 예시로 첨부했습니다.)

 

 - 보통의 경우 time-out에 따라 fade효과가 잘 작동되지만 Modal이 포함되어 있는 컴포넌트 자체가 사라지게될 경우 fade-out이 적용되지 않고 modal이 사라지는 현상이 발생합니다.

 - portal로 컴포넌트를 옮겼지만 해당 컴포넌트를 렌더링 하는 뿌리는 변함이 없기 때문에 발생하는 현상으로 보입니다.

 

 

다른 방법: 전역 Context에 컴포넌트를 직접 전달하기

portal을 통해 렌더링되는 컴포넌트를 이동시키는 것이 아니라 컴포넌트 자체를 함수의 인자로 담아 전역 context에서 렌더링 되게끔 구현했습니다.

 

(이에 대한 아이디어는 아래 포스팅을 참고했습니다.)

https://opensource.com/article/21/5/global-modals-react

 

// App.js
const { openModal, closeModal } = useModal();

const handleModalBtnClick = () => {
    openModal(<Modal>일반모달</Modal>);
  };

- openModal 함수로 모달을 컨텍스트에 전달합니다.

 

//ModalContext.js
export const ModalProvider = ({ children }) => {
  const [modalOpened, setModalOpened] = useState(false);
  const [modalComponent, setModalComponent] = useState(null);

  const openModal = (compoenent) => {
    setModalOpened(true);
    setModalComponent(compoenent);
  };

  const closeModal = () => {
    setModalOpened(false);
    setTimeout(() => {
      setModalComponent(null);
    }, 400);
  };

  return (
    <ModalContext.Provider value={{ openModal, closeModal }}>
      <ModalStateContext.Provider value={{ modalOpened }}>
        {children}
        {modalComponent}
      </ModalStateContext.Provider>
    </ModalContext.Provider>
  );
};

-  modalOpened 상태에 따라 Modal 컴포넌트 내부의 opacity가 조정됩니다. 렌더링 여부에는 관여하지 않습니다.

- closeModal 함수를 실행하면 modalComponent 상태값을 null로 지정해 컴포넌트가 사라집니다. 이때 setTimeout api를 이용해 opacity 변동이 끝난 뒤 렌더트리에서 삭제되게끔 합니다.

 

 

전체코드

 

 

장단점

단순히 렌더링하는 컴포넌트가 사라지는 경우에도 fade-out 효과가 남아있게끔 하는 의도로 작성한 코드이지만 그에 따라 파생되는 장단점이 많이 존재합니다.

 

장점

  • 컴포넌트가 사라지는 경우에도 fade-out 효과 적용  
  • 모달을 사용하는 컴포넌트의 직관적이고 정돈된 jsx
    • 특정 컴포넌트 jsx 내부에 직접 모달 컴포넌트가 포함되지 않고 eventHandler의 인자에 담겨있는것이 모달이 표시되는 타이밍을 직관적으로 나타내줌.

단점

  • 모달을 이용하는 컴포넌트와 모달의 상태연동이 유연하지 않음.
    • 모달이 생성된 이후부터는 상태 업데이트를 모달내부에서 받기 어려움(전역상태를 이용하면 극복가능)
    • Modal이 생성되기전 Modal 내부에 함수로 전달된 상태 업데이트함수 등은 정상작동

 

  • 모달의 전역 context위치에 유의해야함(전역 상태 중 가장 안쪽으로 두어야 안전)
    •  전역에 모달이 생성되는만큼 모달 내부에서 전역상태를 이용할 경우(라우팅 등) 모달 context가 이용하려는 context보다 내부에 있어야함.

 

장단점을 따져보고 목적에 맞게끔 portal을 사용할지 전역에 생성할지 선택하면 될듯 합니다.

 

 

 

선요약

import { ReusableInput } from "../ResuableForm"
// 인풋 컨트롤등 여러 기능을 담고있는 컴포넌트(스타일링은 되어있지않음)

import { UnderLinedInput } from "../inputs"
// 아무기능없이 스타일링만 담고있는 styled-component

const StyledReusableInput = styled(ReusableInput)`
  ${UnderLinedInput.componentStyle.rules}
`;

(props도 잘 적용됨)

 

 

 

굳이 왜 추출해서 확장할까요?

 

보통 아래와 같이 스타일을 확장해서 많이 사용합니다.

import { UnderLinedInput } from "../styles"
// 아무기능없이 스타일링만 담고있는 styled-component

const StyledInput = styled(UnderLinedInput)`
    /*UnderLinedInput에 override할 스타일링 ...*/
    color: blue;
`;

const ReusableInput = () => {
	// ...
	// 컴포넌트 기능을 위한 코드
    // ...
	return (
        <StyledInput>
        {/* ... */}
        </StyledInput>
	)
}

export default ReusableInput

 

- ReusableInput 컴포넌트를 생성할 때 스타일링을 조건부로 작성할 수 있게끔 코드를 짠다면 굳이 styled 객체를 추출해서 적용하는 것은 불필요할 수도 있습니다. (위 코드 예시라면 props로  style을 선택할 수 있게끔 하는 등..)

 

 그렇지만 사후적으로 코드를 수정하기 어려울 수도 있고 ReusableInput의 기능과 스타일링을 분리하고 싶을 수도 있습니다.

 

또한  컴포넌트의 코드를 수정하지 않고 다른 styled component의 스타일을 적용하고 싶을 수도 있습니다. 그러한 상황이라면 style 객체를 추출해서 사용하는것이 좋은 선택지가 될 것입니다.

 

 

 

개발 story

단어장 프로젝트  진행 중 만들었던 reusable한 Form 컴포넌트를 스타일링하는 과정에서 Form 이외의 일반 input에 적용하기 위해 만들었던 styled component의 스타일을 그대로 적용하려고 시도했습니다.

 

다른곳에서 사용한 style을 그대로 또 작성하고 싶진 않았기 때문에 타 styled-component의 css를 추출하는 방법을 구글링 했지만 styled-component는 css를 추출하는 기능을 제공하지 않는다는걸 알게됬습니다 ㅠㅠ

 

당연히 있을법 한 기능인데 지원하지 않는다는 것에 의아해하면서 나름의 방법을 생각해냈습니다.

// 기존 코드
export const UnderLinedInput = styled.input`
  border: none;
  /* 생략... */
  &:focus {
    outline: none;
  }
`;


// 새 코드
const cssForInjection = css`
  border: none;
  /* 생략... */
  &:focus {
    outline: none;
  }
`;
const UnderLinedInput = styled.input`
  ${cssForInjection}
`;

UnderLinedInput.css = cssForInjection
export { UnderLinedInput }

css를 따로작성하고 직접 주입해 확장하고자 하는 곳에서 꺼내 사용하는 형태입니다.

 

잘 작동되었지만 콘솔을 찍어본결과 의외의 상황이 발생..

 

 

직접 주입 않더라도 componentStyle.rules 의 위치에 꺼내 쓸수 있는 형태로 들어가있었습니다...

 

직접주입하는 부분은 삭제하고 간단하게 아래와 같이 추출해서 사용했습니다.

 

import { ReusableInput } from "../ResuableForm"
// 인풋 컨트롤등 여러 기능을 담고있는 컴포넌트(스타일링은 되어있지않음)

import { UnderLinedInput } from "../inputs"
// 아무기능없이 스타일링만 담고있는 컴포넌트

const StyledReusableInput = styled(ReusableInput)`
  ${UnderLinedInput.componentStyle.rules}
`;

 

순수 css로 추출하는 것은 아니지만 그래도 역시 있을 법한 기능은 웬만한 유명한 라이브러리는 가지고 있군요

조금만 더 구글링 했더라면 쉽게 해결했을 문제인것 같지만 그래도 직접 알아낸것에 의미를 두겠습니다!

 

 

+ Recent posts