til

리액트 숙련 1일 차 @컴포넌트 스타일 &&렌더링 최적화 &&Redux

fpzmfks 2024. 8. 16. 21:09

어제 오늘 참 많은 일이 있었다... 어제 지급한 강의를 오늘 11시까지 다 보라는 줄 알고 카페인을 드링킹하며 강의를 보다가 카페인의 힘으로 그만 밤새 잠들지 못했다. 평소 커피를 안 마시다보니 특히 강하게 약발이 든 모양이었다. 24시간을 넘어서 지금 막 한계에 다다른 참이다. 

 

그래도 어찌저찌 강의의 2/3는 잘 이해한 것 같았다. 대부분은 내가 리액트 입문 강의를 듣고 궁금하게 생각했던 것이라 호기심에 더 잘 들을 수 있었던 것도 같다. 

 

먼저 리액트에서 css를 적용하는 것과 관련된 라이브러리, styled-components이다. 이전에 의문으로 생각했던 것이, 리액트 입문 주차에서 튜터님이 제시해주신 방법 중 어떤 것도 개발자에게 있어서 썩 좋은 방법이라는 생각이 들지 않았던 것이다. 기껏 컴포넌트를 나눠서 재사용성을 높여놓았으면 css에서도 재사용성을 고려할 수 있어야하지 않느냐는 것이다. 그리고 이번 숙련 주차에서 이러한 리액트 css에 관한 라이브러리를 권장하며 리액트에서 편리하게 css를 조작하는 법을 알게 되었다. 

 

바로 이런 식이다. 

const StBox = styled.div`
  width: 100px;
  height: 100px;
  border: 1px solid red;
  margin: 20px;
`;

const App = () => {
  return <StBox>박스</StBox>;
};

이렇듯 styled-components를 사용하면 내가 임의의 스타일을 적용한 태그를 만들어서 그대로 적용할 수 있다. 이제까지는 js를 조각으로 나눠서 관리했다면 이제는 css를 나눠서 관리할 수 있는 것이다. 

 

또 전체에 영향을 끼치고 싶은 코드가 있으면 css 코드 조각을 jsx로 분리하여 사용할 수도 있다. 

import { createGlobalStyle } from "styled-components";

const GlobalStyle = createGlobalStyle`
  body {
    font-family: "Helvetica", "Arial", sans-serif;
    line-height: 1.5;
  }
`;

export default GlobalStyle;
return (
    <>
      <GlobalStyle />
      <BlogPost title={title} contents={contents} />
    </>
  );

다음은 렌더링 최적화에 대해 배웠다. 여태껏 페이지 속도에 크게 신경쓸 정도로 여유있게 프로젝트를 진행하지 않았다보니, 이것도 꼭 알아야하는 것 같은데 잘 이해하지 못하겠어서 벼르고 있었던 것이다. 리액트 입문에 들어오면서 렌더링에 대한 언급이 나오면서 이러한 호기심은 커져갔는데, 이번에 memoization에 대해 배우면서 렌더링에 대해 어느 정도 이해하게 된 것 같다. 

 

ux적인 측면에서 말하자면, spa에서 사용자가 체감하는 것들은 대부분 렌더링/리렌더링으로 인한 조작이다. 이러한 리렌더링들이 항상 잘만 작동한다면 문제가 없겠지만, 이는 개발자가 코드를 어떻게 짜느냐에 따라 문제가 생길 수도, 개선될 수도 있는 부분이기에 개발자라면 리렌더링에 대해서도 항상 신경을 써야할 것 같다. 

 

일단 상정할 수 있는 문제는 하나로, 렌더링에 시간이 너무 오래 걸리는 경우이다. 이외에 리렌더링 문제는 내 코드의 문제라는 것이 정설이기 때문에, 이번에 이것만으로 렌더링 최적화를 논해보겠다. 

 

렌더링에 시간이 오래 걸리는 경우, 이는 가져올 페이지의 정보나 계산의 양이 많거나 다른 곳에서 데이터를 가져오는 데에 딜레이가 발생해서 이를 처리하는 데에 시간이 걸리기 때문이다. 이는 순수하게 많은 기능을 가진 페이지이기 때문일 수도 있고, 코드가 비효율적이기 때문일 수도 있다. 어쨌든 최대한 이러한 부분이 개선될 수 있으면 참 좋겠지만, 그래도 개발자이자 사용자로써 말하자면 렌더링에 시간이 좀 걸린다고 해도 최초 한 번 렌더링 될 때의 기다림 정도는 충분히 견딜 수 있다. 정말 불편한 건 리렌더링에 시간이 오래 걸리는 것이다.

 

한 번의 기다림과 여러 번의 기다림은 완전히 다른 경험이다. 만약 정말 재미있는 일을 하려고 하는데 그 일이 시작되기 전에 조금 기다리는 것 정도는 견딜만 한 대가일 것이다. 하지만 한창 재미있는 걸 하고 있는데 난데없이 잠깐 기다린다고 한다면, 이는 제법 불쾌한 일이다. 

 

이러한 불편함을 줄이기 위해서라도 리렌더링 시간을 최적화할 수 있는 것이 바로 memoization이다. 일반적으로 리액트 컴포넌트에서 부모요소가 리렌더링하면 자식요소도 리렌더링한다. 하지만 그러한 자식요소들 중에서는 사실 리렌더링을 하든 안 하든 차이가 없는 것들도 존재하는데, 만약 이러한 리렌더링이 불필요한 요소들 중에 렌더링에 필요한 시간이 많이 걸리는 것이 있다고 한다면, memoization을 통해 해당 요소의 리렌더링을 억제할 수 있다

 

억제한다고는 해도 특정 요소의 변경에 따라 리렌더링을 할 수도 있기 때문에, 잘만 다룬다면 확실히 리렌더링에서 큰 역할을 할 수 있는 기능인 것 같다. 

 


다음은 Redux이다. Redux란 일종의 데이터 관리 방식으로, 전역상태 라이브러리라고 부른다. 이 redux는 styled-components와 마찬가지로 리액트의 추가적인 라이브러리인데, 기존의 props 방식처럼 컴포넌트 사이에서만 데이터를 주고 받지 말고, 아예 다른 곳에 값을 저장해 두었다가 원하는 곳에서 편하게 값을 꺼내올 수 있도록 하는 편의기능이다.

 

입문주차에서 props를 통해 원하는 데이터가 출력될 때까지 계속해서 값을 전달해주어야 했던 props drilling으로부터 나를 구원해줄 기능이다. 

 

기존의 리액트 props의 값 전달 방식에는 문제가 있었는데, 바로 부모요소로부터 물려받은 값을 자식*7요소 정도가 출력하려고 하면 값 전달도 똑같이 7번 정도 해야된다는 것이다. 너무 비효율적인데다가 컴포넌트를 하나씩 작성할 때마다 자식요소에게 넘겨줄 값의 위치와 형태를 정하느라 수고를 끼치게 한다. 심지어는 특별한 오류도 없는데 컴포넌트를 나눴다는 이유로 props로 인한 오류검색이 10여 개씩 뜬 게 바로 내 지난 개인과제의 결과물이다. 내가 감을 못 잡았기 때문이기도 하지만, 이러한 거짓오류의 향연과 번거로움 때문에 나는 지난 개인과제에서 컴포넌트 분리를 적당한 선에서 마무리하고 접었다. 

 

하지만 Redux를 사용하면 기존보다 데이터 전달이 자유로워질뿐만 아니라 컴포넌트 분리와 재사용성 보장에서도 크게 개선될 수 있다. 컴포넌트 분리 시에 데이터 전달과 관련된 부담이 크게 줄었음은 물론, 각 컴포넌트가 항상 어떠한 props를 가지고 있는지 기억하고 신경써줄 필요가 크게 줄어든 것이다. 심지어는 ducks 패턴이나 @reduxjs/toolkit과 같은 것들을 이용하여 하나의 파일에서 원하는 만큼의 데이터를 관리할 수 있게 되어서 내가 원하는 데이터가 어디에 있는지 찾아헤맬 필요가 크게 줄었다. 

 

이렇듯 리액트 숙련주차에서는 리액트를 좀 더 잘 다루는 방법에 대해서 배웠다. 아직 이번 개인과제를 시작하지 않아서 내가 이렇게 배운 것들을 얼마나 활용할 수 있을지 모르겠지만, 최선을 다해서 프로젝트를 완성시킬 것이다. 마지막으로 사소하지만 큰 차이, redux와 @reduxjs/toolkit의 차이에 대한 코드를 남기겠다. 

 

redux

// 액션 타입
const ADD_TODO = "ADD_TODO";
const DELETE_TODO = "DELETE_TODO";

// 액션 생성
export const addTodo = (payload) => {
  return {
    type: ADD_TODO,
    payload,
  };
};

export const deleteTodo = (payload) => {
  return {
    type: DELETE_TODO,
    payload,
  };
};

// 초기값
const initialState = [
  {
    id: 1,
    title: "react를 배워봅시다",
  },
  {
    id: 2,
    title: "redux를 배워봅시다",
  },
];

// 리듀서 함수
const todos = (state = initialState, action) => {
  switch (action.type) {
    case ADD_TODO:
      return [...state, action.payload];
    case DELETE_TODO:
      return state.filter((todo) => todo.id !== action.payload.id);

    default:
      return state;
  }
};

export default todos;

@reduxjs/toolkit

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

// 초기값
const initialState = [
  {
    id: 1,
    title: "react를 배워봅시다",
  },
  {
    id: 2,
    title: "redux를 배워봅시다",
  },
];

// 압축
const todosSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {
    addTodo: (state, action) => {
      return [...state, action.payload];
    },
    deleteTodo: (state, action) => {
      return state.filter((todo) => todo.id !== action.payload.id);
    },
  },
});

export const { addTodo, deleteTodo } = todosSlice.actions;
export default todosSlice.reducer;