계기

 

- 팀 프로젝트 했을 때, 반응형 처리를 못한 게 아쉬워서 간단하게 나마 반응형 처리를 해주었다.

 

 

결과

 

적용 전

 

 

 

 

적용 후

 

- 원래 크기가 어떻게 되든 옆에 있는 메뉴바도 뭉개졌었는데,

일정 크기 이하로 작아지면 1. 메뉴바가 사라지고 2. 게시글 창 크기가 줄어들게끔 작업해주었다.

 

 

 

라이브러리 설치

yarn add @types/react-responsive

 

 

 

 

적용 코드

  // Layout.tsx
  
  const isDeskTop = useMediaQuery({ minWidth: 1200 });
  
    isDeskTop && (
                  <>
                    <SideBar />
                  </>
                  
                  
                  
//ContentBox.tsx

  // 화면 크기에 따라 Container의 너비를 설정
  const isDeskTop = useMediaQuery({ minWidth: 915 });
  
  
   PostTitle: styled.div<{ isDeskTop: boolean }>`
      width: ${(props) => (props.isDeskTop ? '790px' : '500px')};
    margin: 36px 0px 0px 0px;

    display: flex;
    align-items: center;

    color: var(--black, #242424);
    ${(props) =>
      props.isDeskTop
        ? css`
            ${styleFont.titleLarge}
          `
        : css`
            ${styleFont.titleSmall}
          `};
  `,

 

 

 

미리보기

breakpoint에 따라 조건부 지정

 

 

사용법

- 라이브러리 설치

npm install react-responsive
# or
yarn add react-responsive

 

 

export default function Component() {
  const matches = useMediaQuery('(min-width: 768px)')

  return (
    <div>
      {`The view port is ${matches ? 'at least' : 'less than'} 768 pixels wide`}
    </div>
  )
}

 

- 밑에 보이는거와 같이 interface에 여러 매개변수를 사용하지만 주로 쓰이는건 'min-width', 'max-width' 입니다.

- useMediaQuery에 값을 지정해주면 창 크기에 따라 true/false를 반환해준다.

- 그 값에 따라 조건부 랜더링(&&)을 시켜준다.

 

 

Home.tsx

const Home = () => {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    // 창 크기 변경 이벤트 핸들러
    const handleResize = () => {
      setWindowSize({ width: window.innerWidth, height: window.innerHeight });
    };

    // 컴포넌트가 마운트될 때와 언마운트될 때 이벤트 리스너 추가 및 제거
    window.addEventListener("resize", handleResize);
    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);
  return (
    <>
      <p>Window Width: {windowSize.width}px</p>
      <Header />
      <Body/>
    </>
  );
};

- 정확한 수치를 확인할 수 있게 창 사이즈 크기를 띄어주었습니다.

 

 

Header.tsx

const Header = () => {
  const isDesktop = useMediaQuery({ minWidth: 1024 });
  return (
    <>
      <Desktop>
        <div>this is Desktop</div>
      </Desktop>
      <Tablet>
        <div>this is Tablet</div>
      </Tablet>
      <Mobile>
        <div>This is Mobile</div>
      </Mobile>

      <HeaderWrapper>
        <div>
          {/* 목록 버튼 */}
          <button>List Button</button>
        </div>
        {isDesktop && <Logo>로고</Logo>}
        <Nav>
          <ul>
            {/* 메뉴 버튼들 */}
            <li>
              <a href="#">메뉴 1</a>
            </li>
            {isDesktop && (
              <>
                <li>
                  <a href="#">메뉴 2</a>
                </li>
                <li>
                  <a href="#">메뉴 3</a>
                </li>
              </>
            )}
          </ul>
        </Nav>
      </HeaderWrapper>
    </>
  );
};

- desktop사이즈일 때만, 로고 이미지가 보이도록하고,

메뉴 nav바도 모두 보이도록 하였습니다.

 

 

Body

const Body = () => {
  const isDesktop = useMediaQuery({ minWidth: 1024 });
  return (
    <Container>
      <Content>
        {/* 내용을 이곳에 추가 */}
        <h1>내용을 가운데 정렬합니다.</h1>
        <p>기타 내용이 들어갑니다.</p>
      </Content>
      {isDesktop && (
        <Content>
          {/* 내용을 이곳에 추가 */}
          <h1>내용을 가운데 정렬합니다.</h1>
          <p>기타 내용이 들어갑니다.</p>
        </Content>
      )}
    </Container>
  );
};

- 마찬가지로 desktop이면 본론의 모든 내용이 보이도록 하였습니다.

 

 

 

참고

https://usehooks-ts.com/react-hook/use-media-query

 

useMediaQuery

Discover how to use useMediaQuery from usehooks-ts

usehooks-ts.com

개요

데스크톱 컴퓨터, 노트북, 태블릿, 스마트폰 등 다양한 크기와 해상도의 화면을 가진 디바이스를 사용합니다. 반응형 UI를 사용하면 이러한 다양한 화면 크기에 적응하여 사용자 경험을 향상시킬 수 있습니다.

 

유형 소개

반응형 웹 디자인 vs 적응형 웹 디자인


두 가지 방식이 동일해 보일 수 있지만 그렇지 않습니다. 두 접근 방식은 서로를 보완하므로 옳고 그른 방법은 없습니다. 콘텐츠가 결정하게 하세요.

 

 

 

Flow


화면 크기가 작아짐에 따라 콘텐츠가 세로 공간을 더 많이 차지하기 시작하고 아래쪽의 콘텐츠는 아래로 밀려나는데, 이를 흐름이라고 합니다. 픽셀과 포인트를 사용한 디자인에 익숙하다면 이해하기 어려울 수 있지만 익숙해지면 완전히 이해가 됩니다.

 

 

 

 

 

 

 

 

상대적인 요소들 (Relative units)

 

 

 

분기점(Breakpoints)

 

 

 

 

최댓값과 최솟값 (Max and Min values)

가끔 모바일 웹사이트를 열었는데 이미지가 화면에 꽉 차면서 깨지는 경우가 있습니다. 그것은 이미지가 브라우저 넓이보다 작은 데 억지로 늘려놓았기 때문이죠. 이미지 넓이에 최댓값을 설정해준다면 브라우저 크기가 이미지를 크기를 훨씬 넘어가도 더 이상 커지거나 깨지지도 않을 것입니다.

 

 

중첩된 개체


상대적 위치를 기억하시나요? 서로 의존하는 요소가 많으면 제어하기 어렵기 때문에 컨테이너에 요소를 묶으면 훨씬 더 이해하기 쉽고 깔끔하고 정돈된 상태를 유지할 수 있습니다. 이때 픽셀과 같은 정적 단위가 도움이 될 수 있습니다. 픽셀은 로고나 버튼처럼 크기를 조정하고 싶지 않은 콘텐츠에 유용합니다.

 

 

 

 

모바일 혹은 데스크톱 우선 작업 (Mobile or Desktop first)

 

 

 

모바일에서 데스크톱으로 프로젝트가 반응형 작업을 할 때와 데스크톱에서 모바일로 진행될 때 기술적으로 큰 차이는 없습니다. 하지만 모바일에서 먼저 시작할 때 제한이 적고, 콘텐츠 배치를 결정할 때 수월하게 할 수 있습니다. 그래도 프로젝트별로 다를 수 있으니 프로젝트에 맞춰서 해보시길 권합니다.  

 

 

웹폰트와 시스템 폰트 (Webfonts vs System fonts)

 

 

반응형 웹디자인에서 유동적인 레이아웃을 위해 이미지보다 폰트를 많이 쓰는데요. 폰트는 접속 시 내려받아 설치하는 웹폰트와 사용자 기기에 원래 설치된 시스템폰트가 있습니다. 웹폰트를 사용하면 웹사이트를 예쁘게 만들 수 있는 것은 사실입니다. 하지만 웹폰트를 다양하게 쓰는 것은 그만큼 페이지를 느리게 할 수 있습니다. 개인적으로 제목이나 디스플레이를 위한 폰트는 웹폰트로 하고 본문은 시스템폰트를 사용하는 것을 제안합니다.

 

 

 

 

비트맵 방식과 벡터 방식 (Bitmap images vs Vectors)

여러분이 만든 아이콘이 스큐어모픽 스타일처럼 화려하고 디테일하다면 비트맵 방식(jpg, png, gif등등)을 사용하는 것이 맞습니다. 하지만 이런 스타일이 아니라면 벡터방식(svg)을 써보세요. 해상도가 높은 기기에서도 선명하게 보일 것입니다.

 

 

 

출처

https://blog.froont.com/9-basic-principles-of-responsive-web-design/

기본 원리

 

 

Slice 만들기

import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import type { RootState } from "../store/store";
import shortid from "shortid";

// Define a type for the slice state
interface Todo {
  id: string;
  title: string;
  contents: string;
  isDone: boolean;
}
interface Todos {
  todos: Todo[];
}

// Define the initial state using that type
const initialState: Todos = {
  todos: [],
};

export const todoSlice = createSlice({
  name: "todo",
  // `createSlice` will infer the state type from the `initialState` argument
  initialState,
  reducers: {
    addTodo: (
      state,
      action: PayloadAction<{ title: string; contents: string }>
    ) => {
      const newTodo = {
        id: shortid.generate(),
        title: action.payload.title,
        contents: action.payload.contents,
        isDone: false,
      };

      state.todos.push(newTodo);
    },
    deleteTodo: (state, action: PayloadAction<string>) => {
      state.todos = state.todos.filter((todo) => todo.id !== action.payload);
    },
    toggleTodo: (state, action: PayloadAction<string>) => {
      // Find the todo by its id
      const todoToToggle = state.todos.find(
        (todo) => todo.id === action.payload
      );

      // If the todo is found, toggle its 'isDone' property
      if (todoToToggle) {
        todoToToggle.isDone = !todoToToggle.isDone;
      }
    },
  },
});

export const { addTodo, deleteTodo, toggleTodo } = todoSlice.actions;

// Other code such as selectors can use the imported `RootState` type
export const selectTodo = (state: RootState) => state.todos;

export default todoSlice.reducer;

- 액션은 type 필드를 가진 자바스크립트 객체입니다. 쉽게 생각해서, 어떤 일이 일어났는지를 설명하는 이벤트라고 생각하셔도 무방합니다. 보통, type과 payload 프로퍼티를 가지며 type은 어떤 액션인지를 나타내며, payload는 데이터를 담습니다.

- Dispatch는 Store의 내장 함수 중 하나로 액션 객체를 넘겨줘서 상태를 업데이트하는 이벤트 트리거입니다.

호출되면, Store는 Reducer 함수를 실행시켜 Action 처리 로직이 있다면 참고해 새로운 상태를 만듭니다

redux에서 dispatch는 액션을 reducer로 전달합니다. 즉, state를 업데이트하는 유일한 방법은 store.dispatch 함수를 호출하는 것입니다. 이벤트를 발생시키는 역할을 한다고 생각하시면 됩니다.

 

 

Store에 저장하기

import { configureStore } from "@reduxjs/toolkit";
import counterSlice from "../counter/counterSlice";
import todoSlice from "../todos/todoSlice";

export const store = configureStore({
  reducer: {
    counters: counterSlice,
    todos: todoSlice,
  },
});

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;

- RootState및 유형을 각 구성 요소로 가져올 수 있지만 애플리케이션에서 사용할 수 있도록 미리 유형화된 버전의 AppDispatch hooks를 만드는 것이 좋습니다 .

- 이렇게하므로써, 매번 useSelector입력할 필요가 없습니다.(state: RootState)

 

// store/hooks.ts

import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "./store";

// Use throughout your app instead of plain `useDispatch` and `useSelector`
type DispatchFunc = () => AppDispatch;


export const useAppDispatch: DispatchFunc = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

 

 

 

- 실사용

import { useAppDispatch, useAppSelector } from "../store/hooks";
import { decrement, increment, incrementByAmount } from "./counterSlice";

export function Counter() {
  const count = useAppSelector((state) => state.counters.value);
  const dispatch = useAppDispatch();

 

 

참조

https://react-redux.js.org/introduction/getting-started

Async/await , fetch()

전역 fetch() 메서드는 네트워크에서 리소스를 취득하는 절차를 시작하고, 응답이 사용 가능해지면 이행하는 프로미스를 반환합니다.

 

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

const Async = () => {
  const [data, setData] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [isError, setIsError] = useState(false);

  async function getDataByAsync() {
    try {
      const response = await fetch("https://rickandmortyapi.com/api/character");
      const data = await response.json();
      setData(data.results);
      setIsLoading(false);
    } catch (error) {
      setIsError(true);
      setIsLoading(false);
    }
  }

  useEffect(() => {
    getDataByAsync();
  }, []); // useEffect를 사용하여 컴포넌트가 마운트될 때 데이터를 가져옵니다.

  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error loading data</div>;

  return (
    <div>
      <Link to={"-1"}>back</Link>
      <h1>Rick and Morty Characters</h1>
      <ul>
        {data.map((character) => (
          <li key={character.id}>{character.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default Async;

이 코드에서는 React의 useState와 useEffect 훅을 사용하여 데이터 가져오기와 UI 렌더링을 관리하고, try-catch를 사용하여 오류 처리를 수행합니다. 이렇게 하면 데이터를 가져올 때의 비동기 문제를 처리하고, UI를 업데이트하거나 오류 메시지를 표시할 수 있게 됩니다.

 

await 키워드는 async 함수에서만 유효하다

async 함수는 항상 promise를 반환합니다. 

 

 

 

 

AJAX

AJAX (Asynchronous Javascript And XML)

 

//fetch
  const url = "https://rickandmortyapi.com/api/character";
  const options = {
    method: "GET",
    header: {
      Accept: "application/json",
      "Content-Type": "application/json",
      charset: "UTP-8",
    },
  };

  fetch(url, options)
    .then((response) => response.json())
    .then((data) => {
      console.log(data.results);
    })
    .catch((error) => {
      console.error("데이터 가져오기 실패:", error);
    });

 

 

 //axios
  const getData = async () => {
    const response = await axios.get(
      "https://rickandmortyapi.com/api/character"
    );
    return response.data.results;
  };
  const { data, isLoading, isError } = useQuery("characters", getData);
  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error loading data</div>;

  console.log(data)

 

Ajax

AJAX는 비동기식 자바스크립트와 XML(Asynchronous JavaScript And XML)의 약자로, 브라우저가 페이지 전체를 새로 고치지 않고도 서버로부터 데이터를 비동기적으로 로드하고 화면을 업데이트하는 기술입니다. 이를 구현하기 위해 JavaScript를 사용하여 서버와 비동기 통신하며, 주로 XML 데이터를 주고받는 기술로 시작했지만 현재는 JSON을 더 많이 사용합니다.

 

 

  1. Fetch API: Fetch API는 최신 웹 표준으로, 브라우저에서 제공하는 내장 함수로서 간단하고 강력한 HTTP 요청을 만들 수 있습니다. 주로 Promise를 기반으로 하며, 다음과 같이 사용할 수 있습니다.
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => {
    // 데이터 처리 로직
  })
  .catch(error => {
    // 오류 처리 로직
  });

 

 

  1. axios 라이브러리: axios는 브라우저와 Node.js에서 모두 사용 가능한 HTTP 클라이언트 라이브러리로, 더 다양한 기능과 브라우저 호환성을 제공합니다. axios는 Promise 기반으로 작동하며 사용하기 쉽고, 오류 처리 및 요청 취소와 같은 고급 기능을 제공합니다.
import axios from 'axios';

axios.get('https://api.example.com/data')
  .then(response => {
    // 데이터 처리 로직
  })
  .catch(error => {
    // 오류 처리 로직
  });

 

 

  1. POST 요청
    • POST 요청은 데이터를 서버로 보내는 데 사용됩니다.
    • Post를 사용하면 주소창에 쿼리스트링이 남지 않기때문에 GET보다 안전하다.
import axios from 'axios';

const newData = { name: '새로운 데이터', value: 42 };

axios.post('https://api.example.com/data', newData)
  .then(response => {
    console.log('데이터 전송 성공:', response.data);
  })
  .catch(error => {
    console.error('데이터 전송 실패:', error);
  });

 

 

  1. PUT 요청
    • PUT 요청은 서버의 데이터를 업데이트하는 데 사용됩니다.
import axios from 'axios';

const updatedData = { id: 1, name: '수정된 데이터', value: 55 };

axios.put('https://api.example.com/data/1', updatedData)
  .then(response => {
    console.log('데이터 업데이트 성공:', response.data);
  })
  .catch(error => {
    console.error('데이터 업데이트 실패:', error);
  });
  1. DELETE 요청
    • DELETE 요청은 서버의 데이터를 삭제하는 데 사용됩니다.
import axios from 'axios';

axios.delete('https://api.example.com/data/1')
  .then(response => {
    console.log('데이터 삭제 성공:', response.data);
  })
  .catch(error => {
    console.error('데이터 삭제 실패:', error);
  });

 

 

- React Query 사용하면 데이터 관리와 통신을 쉽게 처리할 수 있도록 도와주는 강력한 라이브러리로, 다음과 같은 장점을 제공합니다:

const { data, error, isLoading } = useQuery('todos', async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/todos');
    if (!response.ok) {
      throw new Error('데이터를 불러오지 못했습니다.');
    }
    return response.json();

 

리액트(React)는 사용자 인터페이스(UI) 개발을 위한 JavaScript 라이브러리입니다.

리액트의 핵심 아이디어 중 하나는 가상 DOM입니다. 이것은 실제 DOM의 가벼운 복제본으로, 웹 페이지의 UI를 표현하는 데 사용됩니다.

 

재조정

- 리액트는 가상 DOM을 사용하여 이전 버전과 현재 버전의 UI를 비교하고 변경된 부분을 실제 DOM에 반영합니다. 이 과정을 "재조정"이라고 합니다.

- 재조정은 효율적으로 이루어지며, 변경된 부분만을 업데이트하여 불필요한 DOM 조작을 최소화합니다.

 

상태관리

리액트 컴포넌트는 상태(state)를 가질 수 있습니다. 상태는 컴포넌트의 데이터를 나타내며, 

상태가 변경되면 UI가 다시 렌더링됩니다.

 

 

바벨과 웹팩

리액트로 개발을 할 때는 JSX로 코딩하고,
여기 저기서 모듈들을 export, import 해서 불러옵니다.

 

Babel, WebPack은
1) Babel 이 JSX를 React.createElement() 로 변환해준다.
2) WebPack이 JS, CSS 파일을 번링하여 모듈화해준다.

사용이유

Zustand는 Redux와 같은 전통적인 상태 관리 라이브러리보다 더 간단하게 상태를 관리할 수 있습니다. 상태 및 액션을 정의하기 위한 코드 양이 줄어듭니다.

 

store생성

-라이브러리 설치

npm install zustand
// or
yarn add zustand

 

 

-store 생성, 안에서 store 내부에서 미리 사용할 함수를 선언해줍니다.

// store.ts

import { create } from "zustand";

type CounterStore = {
  count: number;
  increment: () => void;
  decrement: () => void;
};

const useCounterStore = create<CounterStore>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}));

export default useCounterStore;

 

사용

  const { count, increment, decrement } = useCounterStore();

  return (
    <>
      <div>{count}</div>
      <button onClick={increment}>증가</button>
      <button onClick={decrement}>감소</button>
    </>
  );

- jotai , zustand 둘 다 사용해봤는데 jotai는 편리하긴한데 store도 없어서,

완전히 redux와 다른 개념인 느낌이었다.

-zustand는 redux의 개념을 잃어버리지 않게 하는듯하다.

사용이유

쓰로틀링디바운싱은 리소스 사용을 줄이고 성능을 최적화하는 데 도움이 됩니다. 특히 이벤트 핸들러가 많이 호출되는 경우, 이를 제어하여 불필요한 작업을 줄일 수 있습니다. 

 

 

미리보기

 

 

준비물

- 라이브러리 설치

yarn add lodash    // react
yarn add @types/lodash  // typescript

 

 

 

코드

import { debounce, throttle } from "lodash";
import React, { useState } from "react";

const Example = () => {
  const [inputValue, setInputValue] = useState("");
  const [throttleValue, setThrottleValue] = useState("");
  const [debounceValue, setDebounceValue] = useState("");

  // 스로틀링 함수
  const handleThrottle = throttle((value: string) => {
    setThrottleValue(value);
  }, 1000);

  // 디바운싱 함수
  const handleDebounce = debounce((value: string) => {
    setDebounceValue(value);
  }, 1000);

  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const { value } = event.target;
    setInputValue(value);

    // 스로틀링 함수 호출
    handleThrottle(value);

    // 디바운싱 함수 호출
    handleDebounce(value);
  };

  return (
    <div>
      <h1>스로틀링과 디바운싱</h1>
      <input
        type="text"
        placeholder="입력하시오..."
        value={inputValue}
        onChange={handleInputChange}
      />
      <div>
        <h2>Throttle Result (1초당 1글자):</h2>
        <p>{throttleValue}</p>
      </div>
      <div>
        <h2>Debounce Result (타이핑 종료하고 1초 후):</h2>
        <p>{debounceValue}</p>
      </div>
    </div>
  );
};
export default Example;

- 쓰로틀링특정 이벤트가 발생하면 일정 시간 동안 해당 이벤트 핸들러가 호출되지 않고, 주기적으로 이벤트 핸들러가 호출됩니다.

- 디바운싱은 이벤트 핸들러가 마지막 이벤트 발생 이후 일정 시간이 지난 후에 호출되도록 하는 기술입니다.

 

 

 

실사용

 

- useState 훅을 사용하여 사용자의 닉네임의 길이를 15까지 제한 시켰는데..

간혹가다 15를 넘어 16까지 증가되는 현상이 발생했다.

-삽질 결과 해답을 못찾아서 임시방편으로 debouncing을 사용하였다.

 

 

  useEffect(() => {
    // 디바운싱 함수 호출
    if (nickname.length > MAX_NICKNAME_LENGTH) {
      handleDebounce(nickname);
    }

const handleDebounce = debounce((nickname: string) => {
    setNickname((prevNickname) => prevNickname.slice(0, -1));
  }, 10);

-  만약 16자가 입력되면 10/1000 초 후에 현재 닉네임에서 마지막 인덱스에 해당하는 원소를 삭제한다.

- 16자가 -> 15자로 짧은 시간 후에 변경된다.

- 물론, 근본적인 해결이 좋았겠지만, 결국 문제가 해결되어서 다행이다.

+ Recent posts