반응형

react-icons를 이용하면 css 없이 아이콘을 이용할 수 있다.

 

사용방법

Installation (for standard modern project)

npm install react-icons --save

 

 

React Icons

React Icons Include popular icons in your React projects easily with react-icons, which utilizes ES6 imports that allows you to include only the icons that your project is using. Installation (for standard modern project) npm install react-icons --save Usa

react-icons.github.io

react-icons를 프로젝트에 설치했으면 이제 사이트에 들어가서 필요한 아이콘을 찾고 

해당페이지의 부분을 import 해주고 아이콘을 넣어서 사용하면 된다.

 

사이트에 들어가서 ionicons5에 들어가면 IoLogoReact 아이콘이 있다

위에 import 부분을 복사해서 붙여 넣어주고 iconName에 사용할 아이콘이름을 넣어주고 

아이콘을 컴포넌트형식으로  넣어주면 된다. (아이콘을 클릭하면 아이콘이름이 복사됨)

 

App.js

import "./styles.css";
import { IoLogoReact } from "react-icons/io5";
export default function App() {
  return (
    <div className="App">
      <IoLogoReact></IoLogoReact>
    </div>
  );
}

 

 

이번내용은 별로 어려운 것도 없고 설명할 것도 없어서 짧게 끝내겠다 react icon만 설치해 주고 필요한 아이콘을 사이트에서 찾아서 import 해주고 넣기만 하면 끝이다. 

 

반응형
반응형

axios(필수)

 

브라우저와 node.js에서 사용할 수 있는 Promise 기반 HTTP 클라이언트 라이브러리

API: 요청 메소드
GET: 데이터조회
POST: 데이터등록
PUT: 데이터수정
DELETE: 데이터 제거

 

axios 사용법

npm 사용하기:

$ npm install axios

import axios from 'axios';
axios.get('/users/1')
axios.post('경로',{username:"green",id:"aa"})

useState,useEffect로 데이터 로딩하기
상태관리
1.요청의결과 - null
2.로딩상태 - false
3.에러 - null

로딩상태가 true 면 로딩중 하면을 보여줌
에러가 null이 아니면 에러가 발생했습니다 화면을 나타냄
결과값이 없으면 null을 보여줌

로딩이 false 이고 에러가 null이고 결과값이 null이 아닐때 받아온 데이터를 화면에 나타냄 

 

App.js

import "./styles.css";
import { useEffect, useState } from "react";
import axios from "axios";

export default function App() {
  const [data, setdata] = useState(null); //data상태값
  const [loading, setloading] = useState(false); //loading상태값
  const [error, seterror] = useState(null); //
  useEffect(() => {
    const fetchUsers = async () => {
      try {
        //요청이 시작되면 error 와 users를 초기화
        seterror(null); //에러 아직 없음
        setdata(null); //데이터 아직 없음
        //loading상태는 true로 변경
        setloading(true); //로딩중
        const response = await axios.get(
          "https://jsonplaceholder.typicode.com/users"
        );
        setdata(response.data);
      } catch (e) {
        seterror(e);
        console.error(e);
      }
      setloading(false);
    };
    fetchUsers();
  }, []);
  if (loading) return <div>로딩중...</div>;
  if (error) return <div>에러가 발생했습니다.</div>;
  if (!data) return null;

  return (
    <div className="App">
      {data.map((data) => (
        <li key={data.id}>
          {data.username} {data.name}
        </li>
      ))}
    </div>
  );
}

loading이 true이면 로딩중

error에 값이 담겨있으면 에러가 발생했습니다.

data에 데이터가없으면 null을 리턴해준다.

useEffect(() => {
    const fetchUsers = async () => {
      try {
        //요청이 시작되면 error 와 users를 초기화
        seterror(null); //에러 아직 없음
        setdata(null); //데이터 아직 없음
        //loading상태는 true로 변경
        setloading(true); //로딩중
        const response = await axios.get(
          "https://jsonplaceholder.typicode.com/users"
        );
        setdata(response.data);
      } catch (e) {
        seterror(e);
        console.error(e);
      }
      setloading(false);
    };
    fetchUsers();
  }, []);

useEffect로 처음에 마운트될때만 자료를 받아오게해준다. async await으로 비동기문을 만들어주고 try catch로 혹시나 로딩이 정상적으로 이루어지지않았을때 에러처리를 해준다. 

response에 await axios.get(주소를받아올서버주소) // 이렇게 입력해준다 서버주소는 Jsonplaceholder에서 받아왔다.

 

JSONPlaceholder - Free Fake REST API

{JSON} Placeholder Free fake API for testing and prototyping. Powered by JSON Server + LowDB. Tested with XV. As of Oct 2022, serving ~1.7 billion requests each month.

jsonplaceholder.typicode.com

 

이제 이 코드를 좀 더 간결하게 만들어 보겠다. 

useReducer를 사용해서 상태를 관리해주는 코드로 변경

import "./styles.css";
import { useEffect, useReducer } from "react";
import axios from "axios";

const initialstate = {
  data: null,
  error: null,
  loading: false
};
//reducer 함수
function reducer(state, action) {
  switch (action.type) {
    case "LOADING":
      return {
        data: null,
        error: null,
        loading: true
      };
    case "SUCCESS":
      return {
        data: action.data,
        error: null,
        loading: false
      };
    case "ERROR":
      return {
        data: null,
        error: action.error,
        loading: false
      };
    default:
      return state;
  }
}

export default function App() {
  // const [data, setdata] = useState(null);
  // const [loading, setloading] = useState(false);
  // const [error, seterror] = useState(null);
  const [state, dispatch] = useReducer(reducer, initialstate);
  const { data, error, loading } = state;
  useEffect(() => {
    const fetchUsers = async () => {
      try {
        dispatch({
          type: "LOADING"
        });
        const response = await axios.get(
          "https://jsonplaceholder.typicode.com/users"
        );
        dispatch({
          type: "SUCCESS",
          data: response.data
        });
      } catch (e) {
        dispatch({
          type: "ERROR",
          error: e
        });
        console.error(e);
      }
    };
    fetchUsers();
  }, []);
  if (loading) return <div>로딩중...</div>;
  if (error) return <div>에러가 발생했습니다.</div>;
  if (!data) return null;

  return (
    <div className="App">
      {data.map((data) => (
        <li key={data.id}>
          {data.username} {data.name}
        </li>
      ))}
    </div>
  );
}

useState로 줬던 상태값들을 useReducer로 하나로묶어주었고 상태변경도 dispatch를 통해서 하도록 바꿨다.

 

이제여기서 주소에서 값을 받아오는 async await 문을 따로 커스텀 react훅으로 빼서 다른 컴포넌트에서도 주소만 바꿔서 이용할수 있게 만들어주겠다.

useAsync.js

import { useReducer, useEffect } from "react";

//초기값
const status = {
  data: null,
  loading: false,
  error: null
};
//reducer 함수
function reducer(state, action) {
  switch (action.type) {
    case "LOADING":
      return {
        data: null,
        error: null,
        loading: true
      };
    case "SUCCESS":
      return {
        data: action.data,
        error: null,
        loading: false
      };
    case "ERROR":
      return {
        data: null,
        error: action.error,
        loading: false
      };
    default:
      return state;
  }
}
const useAsync = (callback, deps = []) => {
  // callback함수와 deps배열을 받아옴 //
  const [state, dispatch] = useReducer(reducer, status);
  // async aswait
  const fetchUsers = async () => {
    try {
      dispatch({
        type: "LOADING"
      });
      const response = await callback(); //callback 함수는 axios.get(주소) 를 리턴
      dispatch({
        type: "SUCCESS",
        data: response.data
      });
    } catch (e) {
      dispatch({
        type: "ERROR",
        error: e
      });
    }
  };
  useEffect(() => {
    fetchUsers();
  }, deps);
  return [state, fetchUsers]; //상태값과 fetchUsers를 돌려줌
};

export default useAsync;

callback함수에 담기는값

async function getUser() {
  const response = await axios.get(
    "https://jsonplaceholder.typicode.com/users"
  );
  return response;
}

response 를 리턴해주는 비동기 함수를 담는다. useAsync에서 실행시키고 결과값을 response에 저장한다음 data에 담아준다.

 

useAsync가 다 돌아가면 state와 fetchUsers함수를 리턴해준다.

 

App.js

import "./styles.css";
import axios from "axios";
import useAsync from "./Hooks/useAsync";

async function getUser() {
  const response = await axios.get(
    "https://jsonplaceholder.typicode.com/users"
  );
  return response;
}

export default function App() {
  const [state, refetch] = useAsync(getUser, []);
  const { data, error, loading } = state;
  if (loading) return <div>로딩중...</div>;
  if (error) return <div>에러가 발생했습니다.</div>;
  if (!data) return null;

  return (
    <div>
      <ul>
        {data.map((user) => (
          <li ket={user.id}>
            {user.username} {user.name}
          </li>
        ))}
      </ul>
      <button onClick={refetch}>재요청</button>
    </div>
  );
}

 

이렇게 서버에서 값을 받아오는 axios에 대해서 배웠다. 또 함수를 묶어서 커스텀hook을 만들어서 사용할수도 있다는걸 알게되었다 물론 아직 스스로 이런걸 짤수있는 능력은 없지만 읽어서 어떻게 돌아가는지는 아니까 잘 찾아서 따라할수있지않을까? axios는  promise를 기반으로 만들어졌기때문에 async await 말고도 .then으로도 사용할수 있다. 일단 이런건 쓸려면 자바스크립트가 기초가되야되는데 이번에 비동기함수를 보면서 자바스크립트에서 부족한점을 알게된것같다.

반응형
반응형

useReducer )  새로운 상태관리 Hooks

이때까지는 useState로 상태를 만들고 상태 값을 업데이트해 주는 함수를 이용해서 상태를 관리했었다.

useState는 설정하고 싶은 상태를 직접 설정하고 지정해서 상태를 업데이트해 준다. 

 

useReducer는 액션 객체를 기반으로 상태를 업데이트해 준다.

Dispatch --> Reducer --> state

Dispatchstate에 바로 접근할 수 없고 dispatch함수를 호출하고 dispatch에 연결된 reducer함수를 호출하고 action객체를 이용해서 state값에 접근할 수 있다.                  

                             전송

Dispatch(action)  --------> Reducer( state , action ) 

원래 있던 state에서 action이 호출한 값을 받아서 업데이트해 준다.

 

const [ number , dispatch ] = useReducer( 함수,  초기값 ) --> return [ state, dispatch( fn )]

number에 초기값이 담기고 ,

dispatch에 상태를 변경할 수 있는 함수(Reducer)에 접근하는 함수(dispatch)가 담기게 된다.

 

저번에 만든 TodoList에서는 useState로 상태값을 만들어서썻는데 이번에는 useReducer로 만들어보겠다.

 

App.js (useState사용)

mport './App.css';
import Todoheader from './Todoheader';
import Todolists from './Todolist';
import { useState } from 'react';

function App() {
  const [todoState,setTodostate] = useState({
    todoLists:[],
    inputText:""
  })
  const Change = (e)=>{
    setTodostate({
      ...todoState, //원래있던 todostate값을 저장
      inputText: e.target.value
    });
  };
  const Addlist=()=>{
    const newtodolist = [
      ...todoState.todoLists,
      {id:todoState.todoLists.length,text:todoState.inputText}
    ]
    setTodostate({
        todoLists:newtodolist,
        inputText:""
    })
  }
  const delTodoLists = (id)=>{
    const newtodolist = todoState.todoLists.filter(list=>list.id !== id);
    setTodostate({
      ...todoState,
      todoLists:newtodolist
    })  
  }
  return (
    <div className="App">
      <Todoheader
      inputText={todoState.inputText} //value로 들어갈값
      Change ={Change}
      Addlist={Addlist}      
      />
      <Todolists
      todoLists = {todoState.todoLists} // li를 만들기위해 리스트값을 받아옴
      deltodolists = {delTodoLists}
      />
    </div>
  );
}

export default App;

Todoheader.js

import React from 'react'

const Todoheader = ({inputText,Change,Addlist})=>{

    return(
        <div className='header'>
            <h2>To do List</h2>
            <div>
                <input value={inputText} onChange={Change}/>
                <button onClick={Addlist}>+</button>
            </div>
        </div>
    )
}

export default Todoheader

 Todolists.js

import React from 'react'

const Todoheader = ({inputText,Change,Addlist})=>{

    return(
        <div className='header'>
            <h2>To do List</h2>
            <div>
                <input value={inputText} onChange={Change}/>
                <button onClick={Addlist}>+</button>
            </div>
        </div>
    )
}

export default Todoheader

여기까지는 전에 했던 todolist의 코드이다. 이제 여기서 useState로 만들어줬던 상태값을 useReducer로 만들고 상태변환 함수도 reducer라는 이름의 함수로 만들어줘야 한다. 

 

**상태변환함수의 이름은 reducer가 아니어도 상관없다 편의에 따라 붙일 수 있는 함수의 이름이다

 

App.js (useReducer사용)

import "./styles.css";
import Header from "./Header";
import Todolist from "./Todolist";
import { useReducer } from "react";
//초기값
const initialState = {
  todoLists: [],
  inputText: ""
};
//상태 변환함수 action객체를 받아서 조건에따라 상태를 변경
function reducer(state, action) {
  switch (action.type) {
    case "changeInput":
      return {
        ...state,
        inputText: action.payload
      };
    case "addTodo":
      return {
        ...state,
        todoLists: [...state.todoLists, action.todo],
        inputText: ""
      };
    case "deleteTodo":
      return {
        ...state,
        todoLists: state.todoLists.filter((list) => list.id !== action.id)
      };
    case "toggleTodo":
      return {
        ...state,
        todoLists: state.todoLists.map((todo) =>
          todo.id === action.id ? { ...todo, isDone: !todo.isDOne } : todo
        )
      };
    default:
      return state; //조건에 일치하는게 없을 경우 원래의 상태값을 반환 변화 X
  }
}
export default function App() {
  // 상태선언
  const [state, dispatch] = useReducer(reducer, initialState); //상태변화함수 , 초기값값
  //상태값구조분해 할당
  const { todoLists, inputText } = state;
  const Change = (e) => {
    dispatch({
      type: "changeInput",
      payload: e.target.value
    });
  };
  const Addlist = () => {
    dispatch({
      type: "addTodo",
      todo: { id: todoLists.length, text: inputText, isDone: false }
    });
  };
  const deltodolists = (id) => {
    dispatch({
      type: "deleteTodo",
      id: id
    });
  };
  const toggleTodo = (id) => {
    dispatch({
      type: "toggleTodo",
      id: id
    });
  };

  return (
    <div className="App">
      <Header inputText={inputText} Change={Change} Addlist={Addlist} />
      <Todolist
        todoLists={todoLists}
        deltodolists={deltodolists}
        toggleTodo={toggleTodo}
      />
    </div>
  );
}

여기서는 우선 App컴포넌트 위에 상태값으로 사용할 초기값을 변수로 만들어주고 상태변환함수로 사용할 reducer 도 만들어줬다 reducerdispatch에서 보내주는 action 객체를 받아서 상태값을 return 해준다. action 객체 의 type을 스위치문의 조건으로 사용해서 조건에 맞을 때 상태를 넣어준다.

 

컴포넌트에서 상태값을 만들 때 useState처럼 구조분해할당으로 상태값상태변환함수를 받아주고 

useReducer(reducer함수, 초기값 )을 넣어준다.

// 상태선언
const [state, dispatch] = useReducer(reducer, initialState); //상태변화함수 , 초기값값
//상태값구조분해 할당
const { todoLists, inputText } = state;

 

그리고 아래에서는 HeaderTodolist Props 로 보내줄 값과 함수를 만들어서 보내준다. 

함수는 dispatch함수를 이용해서 필요한 값이 들어간 객체를 리턴 시켜주면 reducer 에서 받아서 상태값을 변화시켜 준다.

 

useReducer를 사용하면 상태를 더 관리하기 쉽고 보기 편하다고 했는데 지금은 그런지 전혀 모르겠다. 나중에 ContextAPI와 같이 사용하면 조금 더 깔끔하게 사용이 가능하다.. 

 

 

 

다음은 ContextAPI를 이용해서 dispatch를 보내주고 각 컴포넌트에서 받아서 쓸 수 있게 해 주겠다

그러면 app에서 함수를 일일이 만들어서 props로 보내지 않아도 

각 컴포넌트에서 dispatchreducer함수를 쓸 수 있고 상태값을 바꿀 수 있게 된다.

 

컴포넌트 바깥 부분에 Context를 만들어주고.↓

export const UserDispatch = createContext();

 

Context태그로 감싸주고 값으로 dispatch함수를 넣어준다

이제 태그내부의 컴포넌트에서 useContext 를 써서 dispatch함수를 사용할 수 있다.↓

  return (
    <UserDispatch.Provider value={dispatch}>
      <div className="App">
        <Header inputText={inputText} id={id} />
        <Todolist todoLists={todoLists} />
      </div>
    </UserDispatch.Provider>
  );

 

Header.js

import React, { useContext } from "react";
import { UserDispatch } from "./App"; //Context가져오기

const Header = ({ inputText, id }) => {
  const dispatch = useContext(UserDispatch); //useContext를 써서 Context값 사용 dispatch함수가 담김
  return (
    <div className="header">
      <h2>To do List Reducer</h2>
      <div>
        <input
          value={inputText}
          onChange={(e) => {
            dispatch({
              type: "changeInput",
              payload: e.target.value
            });
          }}
        />
        <button
          onClick={() => {
            dispatch({
              type: "addTodo",
              todo: { id: id, text: inputText, isDone: false }
            });
          }}
        >
          +
        </button>
      </div>
    </div>
  );
};

export default Header;

이런 방법으로 원래는 함수를 App컴포넌트에서 만들어서 보내줬었지만 dispatch함수를 각 컴포넌트에서 사용할 수 있게 되면 컴포넌트에서 dispatch를 사용해서 상태를 변경시킬 수 있다.

 

App.js (ContextAPI로 dispatch 보내기)

import "./styles.css";
import Header from "./Header";
import Todolist from "./Todolist";
import { createContext, useReducer } from "react";
//초기값
const initialState = {
  todoLists: [],
  inputText: "",
  id: 1
};
//상태 변환함수 action객체를 받아서 조건에따라 상태를 변경
function reducer(state, action) {
  switch (action.type) {
    case "changeInput":
      return {
        ...state,
        inputText: action.payload
      };
    case "addTodo":
      return {
        ...state,
        todoLists: [...state.todoLists, action.todo],
        inputText: ""
      };
    case "deleteTodo":
      return {
        ...state,
        todoLists: state.todoLists.filter((list) => list.id !== action.id)
      };
    case "toggleTodo":
      return {
        ...state,
        todoLists: state.todoLists.map((todo) =>
          todo.id === action.id ? { ...todo, isDone: !todo.isDOne } : todo
        )
      };
    default:
      return state; //조건에 일치하는게 없을 경우 원래의 상태값을 반환 변화 X
  }
}
//context만들기
export const UserDispatch = createContext();
export default function App() {
  // 상태선언
  const [state, dispatch] = useReducer(reducer, initialState); //상태변화함수 , 초기값값
  //상태값구조분해 할당
  const { todoLists, inputText, id } = state;
  // props로 보내주던 함수들 이제는 context로 각 컴포넌트에서 dispatch를 사용해서 만들어서보내주지않아도된다
  // const Change = (e) => {
  //   dispatch({
  //     type: "changeInput",
  //     payload: e.target.value
  //   });
  // };
  // const Addlist = () => {
  //   dispatch({
  //     type: "addTodo",
  //     todo: { id: todoLists.length, text: inputText, isDone: false }
  //   });
  // };
  // const deltodolists = (id) => {
  //   dispatch({
  //     type: "deleteTodo",
  //     id: id
  //   });
  // };
  // const toggleTodo = (id) => {
  //   dispatch({
  //     type: "toggleTodo",
  //     id: id
  //   });
  // };

  return (
    <UserDispatch.Provider value={dispatch}>
      <div className="App">
        <Header inputText={inputText} id={id} />
        <Todolist todoLists={todoLists} />
      </div>
    </UserDispatch.Provider>
  );
}

 

Header.js

import React, { useContext } from "react";
import { UserDispatch } from "./App";

const Header = ({ inputText, id }) => {
  const dispatch = useContext(UserDispatch);
  return (
    <div className="header">
      <h2>To do List Reducer</h2>
      <div>
        <input
          value={inputText}
          onChange={(e) => {
            dispatch({
              type: "changeInput",
              payload: e.target.value
            });
          }}
        />
        <button
          onClick={() => {
            dispatch({
              type: "addTodo",
              todo: { id: id, text: inputText, isDone: false }
            });
          }}
        >
          +
        </button>
      </div>
    </div>
  );
};

export default Header;

Header컴포넌트에서 useContextdispatch함수를 받아와서 reducer함수에 접근해서

상태값을 바꿀 수 있다.

 

Todolist.js 

import React, { useContext } from "react";
import { UserDispatch } from "./App";

const Todolist = ({ todoLists }) => {
  const dispatch = useContext(UserDispatch);
  return (
    <div>
      <ul>
        {todoLists.map((list) => (
          <li key={list.id} style={{ color: list.isDone ? "#eee" : "#333" }}>
            <span
              onClick={() => {
                dispatch({
                  type: "toggleTodo",
                  id: list.id
                });
              }}
            >
              {list.text}
            </span>
            <button
              onClick={() =>
                dispatch({
                  type: "deleteTodo",
                  id: list.id
                })
              }
            >
              삭제
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
};
export default Todolist;

 

아직은 좀 익숙치 않기는 한데 몇 번 더쓰고나면 useState만큼 편하게 쓸수있지않을까., 많은 상태값을 한번에 관리 하기는 좋은것같다.

반응형
반응형

useCallback 은 useMemo와 비슷한 React-Hook이다. 값을 메모리에 저장해 두고 일반적으로 리렌더링 될 때에는 초기화되지 않고 값이 변경되는 것을 감지해서 값이 변경될 때만 함수를 실행시켜 주고 값을 업데이트해 준다.

 

차이점

useMemo(콜백함수, [ ]) 을 memoization (메모리에 저장)
useCallback(콜백함수, [ ]) 함수를 memoziation (메모리에 저장)

 

useCallback을 쓰는 이유 

 

자바스크립트에서 함수는 객체의 한 종류로 취급된다. 그래서 리렌더링 되면 함수가 초기화되면서 새로운 주소값을 할당받기 때문에 이전과 같은 모양의 함수라도 다른 함수로 취급된다. 그래서 함수를 저장하고 값이 변경되었을 때만 함수를 바꿔주는 hooks이 필요하다.

 

예제) 크기가 바뀌는 박스, 다크모드 

App.js

import { useState } from "react";
import Box from "./Box";
import "./styles.css";

export default function App() {
  const [size, setSize] = useState(100);
  const createBoxStyle = () => {
    return {
      width: `${size}px`,
      height: `${size}px`,
      backgroundColor: "turquoise"
    };
  };
  return (
    <div>
      <input
        type="number"
        value={size}
        onChange={(e) => {
          setSize(e.target.value);
        }}
      />
      <Box createBoxStyle={createBoxStyle} />
    </div>
  );
}

Box.js

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

const Box = ({ createBoxStyle }) => {
  const [style, setStyle] = useState({});

  useEffect(() => {
    console.log("👍👍박스 키우기");
    setStyle(createBoxStyle()); //스타일에 함수가반환하는 스타일값을 넣어줌
  }, [createBoxStyle]); //createboxstyle함수가 변경될때만 실행

  return <div style={style}></div>;
};

export default Box;

이렇게 인풋태그의 숫자가 변경되면 박스의 크기가 변경되고 useEffect로 createBox함수가 바뀔 때마다 '박스 키우기'가 콘솔에 뜨게 해 줬다. 지금은 이 코드가 잘 작동하지만 상태값을 하나 더 추가하면 문제가 생긴다. 

 

App.js (dark상태값추가 버튼태그추가)

import { useState } from "react";
import Box from "./Box";
import "./styles.css";

export default function App() {
  const [size, setSize] = useState(100);
  const [dark, setDark] = useState(false);
  const createBoxStyle = () => {
    return {
      width: `${size}px`,
      height: `${size}px`,
      backgroundColor: "turquoise"
    };
  };
  const style = {
    backgroundColor: dark ? "#333" : "#fff",
    color: dark ? "#fff" : "#333"
  };
  return (
    <div style={style}>
      <div>
        <input
          type="number"
          value={size}
          onChange={(e) => {
            setSize(e.target.value);
          }}
        />
        <Box createBoxStyle={createBoxStyle} />
        <button
          onClick={() => {
            setDark(!dark);
          }}
        >
          모드변경
        </button>
      </div>
    </div>
  );
}

이렇게 dark라는 상태값을 하나 추가해 주고 버튼을 누르면 상태값이 변경된다. 모드변경모드를 누를 때마다 상관없는 박스 키우기가 콘솔에 뜨게 되는데 이유는 dark상태값이 변경되면서 컴포넌트가 리렌더링 되고 createBox함수도 리렌더링 되면서 같은 모양의 함수이지만 다른 주소를 가지게 되면서 Box컴포넌트에서 useEffect가 다른 함수라고 인식하게 되어 실행되어서 그렇다. 그래서 이를 막기 위해서는 함숫값이 변경되지 않게 해줘야 한다. 사이즈가 변경될때만 함수를 다시호출하고 변경되지않을 때는 저장되어있는 함수를 그대로 이용해서 같은주소의 함수가 보내지게 해줘야한다.

 

useCallback을 이용해서 함수를 저장해 준다.

 

App.js

import { useCallback, useState } from "react";
import Box from "./Box";
import "./styles.css";

export default function App() {
  const [size, setSize] = useState(100);
  const [dark, setDark] = useState(false);
  const createBoxStyle = useCallback(() => { //박스스타일을 만들어주는 함수를 저장 
    return {								//size상태가 변경되면 실행
      width: `${size}px`,
      height: `${size}px`,
      backgroundColor: "turquoise"
    };
  }, [size]);

  const style = {
    backgroundColor: dark ? "#333" : "#fff",
    color: dark ? "#fff" : "#333"
  };
  return (
    <div style={style}>
      <div>
        <input
          type="number"
          value={size}
          onChange={(e) => {
            setSize(e.target.value);
          }}
        />
        <Box createBoxStyle={createBoxStyle} />
        <button
          onClick={() => {
            setDark(!dark);
          }}
        >
          모드변경
        </button>
      </div>
    </div>
  );
}

이렇게 써주면 useCallback이 boxstyle을 주던 함수를 저장하게 되고 size가 변경될 때만 함수가 다시 만들어진다.

사이즈가 변경되지 않았을 때는 원래 있던 함수를 box컴포넌트에 보내주고 주소가 이전과 같기 때문에 useEffect에서도 다시 실행되지 않는다.

 

 

저번 useMemo와 같이 useCallback도 값을 저장해 뒀다가 쓰는 방식이기 때문에 많이 쓰면 자원을 낭비하게 된다. 꼭 필요할 때 적절히 쓸 수 있도록 해야 한다. 아직은 언제 어떤 방식으로 써야 적절하게 쓰는 건지 감이 잘 오지는 않는다. 나중에 포트폴리오를 하면서 쓰다가 보면 감이 오겠지.

반응형
반응형

 

useMemo


useMemo
동일한 값을 리턴하는 함수를 반복적으로 호출해야 한다면 처음값을 계산할 때
그 값을  메모리에 저장해서 필요할때만 계산을 하고, 일반적으로 리렌더링 되는상황에서는

메모리에서 꺼내서 재사용하는 기법 자주 필요한 값을 처음 계산할 때 캐시에 저장을 해두어  
값이 필요할 때마다 다시 계산하지 않고 캐시에서 꺼내서 재사용. 값이 변경되면 다시 저장

 

사용법

구문 * deps == 의존성배열

useMemo(( )=>{
	return value;
},[deps])

콜백함수와 , 배열을 인자로 받아서 사용한다,

콜백함수는 메모이제이션(메모리에저장) 해줄 값을 리턴해주는 함수,

배열은 의존성 배열이라고도 부르는데 배열 안의 값이 업데이트될 때만 콜백함수를 호출해서

메모이제이션 된 값을 업데이트해서 다시 메모이제이션 해준다.

(저장된 값을 불러와서 변경한 후 다시 저장한다는 소리.) ,

의존성 배열이 없으면 처음마운트될 때 값이 메모이제이션되고 필요할 때마다 같은 값을 사용한다

(바뀌었는지 확인할 수 있는 값이 없으니 처음에 저장된 값을 계속 사용.)

 

App.js

import './App.css';
import { useState } from 'react';
import ShowState from './components/ShowState';

function App() {
  const [number,setNumber] = useState(0);
  const [text,setText] = useState("");
  const increaseNumber = ()=>{
    setNumber(number+1)
  }
  const decreaseNumber = ()=>{
    setNumber(number-1)
  }
  const onChange = (e)=>{
    setText(e.target.value)
  }

  return (
    <div className="App">
      <div>
        <button onClick={increaseNumber}>+1</button>
        <button onClick={decreaseNumber}>-1</button>
        <input type="text" placeholder='lastname' value={text} onChange={onChange}/>
      </div>
      <ShowState number={number} text={text}></ShowState>
    </div>
  );
}

export default App;

ShowState.js

import React from 'react';

const getNumber = (number)=>{
    console.log("숫자가 변동되었습니다.");
    return number
}
const getText = (text) =>{
    console.log("글자가 변동되었습니다.")
    return text;
}
const ShowState = ({number,text}) => {
    const showNumber = getNumber(number);
    const showText = getText(text);
    return (
        <div>
            {showNumber}<br/>
            {showText}
        </div>
    );
};
export default ShowState;

숫자를 변경해도 글자가 변동되었다는 문자가 같이 나온다

상태값이 변경될 때 컴포넌트가 리렌더링 되는데 다시 렌더링 되면서 함수를 다시 전부 실행해 줘서이다.

 

+1 버튼을 클릭하면 increaseNumber함수가 실행되고 setNumbernumber상태를 1 증가시켜 준다

상태가 변경되면서 리렌더링 되고 App, ShowState 컴포넌트를 다시 불러온다.

 

ShowState컴포넌트 내부의 코드

const showNumber = getNumber(number);
const showText = getText(text);

getNumber와 getText함수를 다시 실행하기 때문에 콘솔에 로그가 뜨게 된다. 

 

ShowState.js (useMemo사용)

import React, { useMemo } from "react";

const getNumber = (number) => {
  console.log("숫자가 변동되었습니다.");
  return number;
};
const getText = (text) => {
  console.log("글자가 변동되었습니다.");
  return text;
};
const ShowState = ({ number, text }) => {
  const showNumber = useMemo(() => {
    return getNumber(number);
  }, [number]); //number가 변경될때만 getNumber가 다시 실행된다.
  // const showText = useMemo(() => {
  //   return getText(text);
  // }, [text]);
  const showText = getText(text); //상태값이 변경되서 리렌더링 될때마다 getText다시실행행
  return (
    <div>
      {showNumber}
      <br />
      {showText}
    </div>
  );
};

export default ShowState;

useMemo를 사용하게 되면 의존성배열에 있는 값을 메모리에 저장했다가

값에 변화가 있을 때에만 콜백함수를 실행해서 값을 업데이트하고 다시 메모리에 저장해 준다.

위 코드에서처럼 useMemonumber값을 메모리에 저장하고 number값이 변경될 때만 실행된다

input에 글자를 입력하는 경우는 number값에 변화가 없기 때문에 getNumber함수가 실행되지 않는다.

 

반대로 showText값은 아까처럼  어떠한 상태값이 변경되어서 리렌더링 될 때마다

getText함수를 실행시켜서 콘솔에 

"글자가 변경되었습니다"라는 글이 나오게 된다

그렇지만 useMemo를 이렇게 사용하는 것은 별로 좋은 사용법이 아니다.

메모리의 자원을 점유하게 되기 때문에 자원을 낭비하게 되는 것이다.

꼭 필요한 경우에만 사용할 수 있도록 하자!! 

지금 같은 코드는 useEffect로 값이 변경될 때에만 실행시켜 주는 방식으로 충분히 문제를 예방할 수 있다.

 

ShowState.js (useEffect사용)

import React, { useEffect } from "react";

const getNumber = (number) => {
  console.log("숫자가 변동되었습니다.");
  return number;
};
const getText = (text) => {
  console.log("글자가 변동되었습니다.");
  return text;
};
const ShowState = ({ number, text }) => {
  const showNumber = useEffect(
    (number) => {
      return getNumber(number);
    },
    [number]
  );
  const showText = useEffect(
    (text) => {
      return getText(text);
    },
    [text]
  );
  return (
    <div>
      {showNumber}
      <br />
      {showText}
    </div>
  );
};

export default ShowState;

이렇게 메모리를 사용하지 않고도 해결할 수 있기 때문에 useMemo를 사용할 일이 많지는 않지만 

useEffect로 의존배열을 받을 때 객체는 넣어도 구분할 수 없다는 문제점이 있어서 이때는 useMemo를 사용해서 해결해 줄 수 있다.

 

App.js

import "./styles.css";
import React, { useEffect, useState } from "react";

const MemoComponent = () => {
  const [number, setNumber] = useState(0);
  const [isKorea, setIsKorea] = useState(true);

  const location = isKorea ? "한국" : "외국";
  //함수가 호출될때마다 항상 새로운객체가 생성됨
  // useEffect -->마운트될때, 리렌러링될때(업데이트) ,언마운트될때
  useEffect(() => {
    console.log("useEffect 호출");
  }, [location]);
  // 마운트될때와 location이변경될때 실행됨
  return (
    <div className='App'>
      <h2>좋아하는 숫자는?</h2>
      <input
        type="number"
        value={number}
        onChange={(e) => setNumber(e.target.value)}
      />
      <h2>이동하실까요</h2>
      <p>나라: {location}</p>
      <button onClick={() => setIsKorea(!isKorea)}>이동</button>
    </div>
  );
};

export default MemoComponent;

아까처럼 useEffect를 사용해서 숫자가 변경될 때에 location 값이 변경될 때에만 "useEffect를 사용 중입니다."라는 코드를 실행하게 해 주었다.

 

 

지금은 의존성 배열에 문자열값이 들어가는데 만약 여기에 객체타입이 들어간다면 어떻게 될까?

App.js

import "./styles.css";
import React, { useEffect, useState } from "react";

export default function App() {
  const [number, setNumber] = useState(0);
  const [isKorea, setIsKorea] = useState(true);

  const location = {
    country: isKorea ? "한국" : "외국"
  };

  useEffect(() => {
    console.log("useEffect 호출");
  }, [location]);

  return (
    <div className="App">
      <div>
        <h2>좋아하는 숫자는?</h2>
        <input
          type="number"
          value={number}
          onChange={(e) => setNumber(e.target.value)}
        />
        <h2>이동하실까요</h2>
        <p>나라: {location.country}</p>
        <button onClick={() => setIsKorea(!isKorea)}>이동</button>
      </div>
    </div>
  );
}

location을 객체로 반환받게 만들어줬다

타입이 객체로 바뀐 것뿐인데 숫자를 바꿔도 'useEffect호출'이 콘솔에 뜨는 걸 볼 수 있다.

 

이건 타입에서 생기는 문제인데 string타입은 변수에 값이 바로 담겨있기 때문에 useEffect에서 봤을 때

같은 값이면 실행시키지 않는다.

그러나 객체타입은 값을 바로 가지는 게 아니고 주소값을 가지기 때문에 상태값이 변경되어 리렌더링이 일어나고 location이 다시 실행되어서 같은 모양의 객체를 만들어도 가지고 있는 주소값이 이전과 다르기 때문에 useEffect에서 보기에는 다른 값이라고 여기게 되어서 리렌더링이 일어날 때마다

안의 console.log가 실행되게 된다.

 

이럴 때에는 useMemo를 이용해서 isKorea 가 변경될 때만

location이 실행되어서 country를 받을 수 있게 해 준다.

 

App.js

import "./styles.css";
import React, { useEffect, useMemo, useState } from "react";

export default function App() {
  const [number, setNumber] = useState(0);
  const [isKorea, setIsKorea] = useState(true);

  const location = useMemo(() => {
    return {
      country: isKorea ? "한국" : "외국"
    };
  }, [isKorea]);
  useEffect(() => {
    console.log("useEffect 호출");
  }, [location]);

  return (
    <div className="App">
      <div>
        <h2>좋아하는 숫자는?</h2>
        <input
          type="number"
          value={number}
          onChange={(e) => setNumber(e.target.value)}
        />
        <h2>이동하실까요</h2>
        <p>나라: {location.country}</p>
        <button onClick={() => setIsKorea(!isKorea)}>이동</button>
      </div>
    </div>
  );
}

이렇게 써주면 숫자가 변경될 때는 isKorea가 변하지 않기 때문에 {country: isKorea? '한국':'외국'} 구문을 실행하지 않게 되고 useEffect에서는 새로 객체를 만들지 않으니 똑같은 값이기 때문에 useEffect호출이 콘솔 로그되지 않는다.

 

아직 useMemo개념은 언제 써야 할지 잘 모르겠다... 조금 더 hooks에 익숙해지고 나면 잘 쓸 수 있겠지.. 지금 이 내용도 그때 다시 포스팅을 올려서 더 알기 쉬운 포스팅이 되었으면 좋겠다..

반응형
반응형

1.Styled-Component 코드 리팩토링 

2.Polish 사용

 

저번에 만들었던 styled-component코드를 조금 정리하고 hover 되었을 때 파란색으로만 변하는 효과를

Polished 를 사용해서 고쳐주었다.

 

ButtonTotal.js

import React from "react";
import styled, { css } from "styled-components";

const ButtonTotal = ({ children, size, color, ...rest }) => {
  const StyledButton = styled.button`
    /*공통스타일*/
    display: inline-flex;
    outline: none;
    border: none;
    border-radius: 4px;
    color: white;
    font-weight: bold;
    cursor: pointer;
    padding: 2em;
    margin: 10px;

    /*크기*/
    font-size: 1em;
    /* 조건별색깔 */
    ${({ theme, color }) => {
      const selectcolor = theme.palette[color];
      return css`
        background: ${selectcolor};
      `;
    }}

    /*조건별크기*/
    ${(props) => {
      return (
        props.size === "large" &&
        css`
          height: 3.5em;
          font-size: 1.5em;
          width: 40%;
        `
      );
    }}
    ${(props) => {
      return (
        props.size === "medium" &&
        css`
          height: 2.25em;
          font-size: 1em;
        `
      );
    }}
    ${(props) => {
      return (
        props.size === "small" &&
        css`
          height: 1.5em;
          font-size: 0.75em;
        `
      );
    }}
    /*효과*/
    &:hover {
      background: #1c7ed6;
    }
  `;

  return (
    <StyledButton size={size} color={color} {...rest}>
      {children}
    </StyledButton>
  );
};
ButtonTotal.defaultProps = {
  size: "medium",
  color: "blue"
};
export default ButtonTotal;

전에썻던 코드이다 StyledButton 쪽 코드에 조건으로 주는 값이 많아서 읽기 불편한 것 같다.

 /*조건별크기*/
    ${(props) => {
      return (
        props.size === "large" &&
        css`
          height: 3.5em;
          font-size: 1.5em;
          width: 40%;
        `
      );
    }}
    ${(props) => {
      return (
        props.size === "medium" &&
        css`
          height: 2.25em;
          font-size: 1em;
        `
      );
    }}
    ${(props) => {
      return (
        props.size === "small" &&
        css`
          height: 1.5em;
          font-size: 0.75em;
        `
      );
    }}

위의 이 코드를 지우고 스타일 컴포넌트 윗줄코드에

// 사이즈
    const sizes ={
        large:{
            height: '3em',
            fontSize: '1.25em',
            width:'40%'
        },
        medium:{
            height: '2.5em',
            fontSize: '1em',
            width:'25%'
        },
        small:{
            height: '1.75em',
            fontSize: '1em',
            width:'15%'
        },
    }
    const sizestyle = css`
    ${({size})=>css`
    height:${sizes[size].height};
    font-size:${sizes[size].fontSize};
    width:${sizes[size].width};
    `}
    `;

이렇게 사이즈로 들어갈 sizes객체를 하나 만들고ButtonTotal에 props로 받아오는 sizecss로 작성해서 sizestyle에 저장해 준다. 그러면 스타일이 변수에 저장되게 되고 변수를 스타일 컴포넌트에 넣어주면 스타일 컴포넌트가 만들어지기 전에 미리 조건으로 스타일을 정해서 컴포넌트에 넣어주게 된다.

 

색깔지정코드

/* 조건별색깔 */
    ${({ theme, color }) => {
      const selectcolor = theme.palette[color];
      return css`
        background: ${selectcolor};
      `;
    }}

이 코드도 스타일 컴포넌트 밖으로 빼서 변수에 css를 저장해 준 뒤에

저장된 변수만 스타일컴포넌트에 넣어주겠다.

const colorStyle=css`
    ${({theme,color})=>{
        const selected = theme.palette[color];
        return css`
        background: ${selected};
        &:hover {
            background:${lighten(0.2,selected)}
        }
        `;
    }}`;

위와 똑같이 스타일컴포넌트가 만들어지기 전에 ButtonTotal에서 받은 props를 이용해서 스타일을 만들고 css를 colorstyle에 저장해 주었다

 

완성된 코드

import React from 'react';
import styled,{css} from 'styled-components';
import { lighten } from 'polished';

const ButtonTotal = ({children,color,size,fullWidth,...res}) => {
    //배경색변수
    const colorStyle=css`
    ${({theme,color})=>{
        const selected = theme.palette[color];
        return css`
        background: ${selected};
        &:hover {
            background:${lighten(0.2,selected)}
        }
        `;
    }}`;
    // 사이즈
    const sizes ={
        large:{
            height: '3em',
            fontSize: '1.25em',
            width:'40%'
        },
        medium:{
            height: '2.5em',
            fontSize: '1em',
            width:'25%'
        },
        small:{
            height: '1.75em',
            fontSize: '1em',
            width:'15%'
        },
    }
    const sizestyle = css`
    ${({size})=>css`
    height:${sizes[size].height};
    font-size:${sizes[size].fontSize};
    width:${sizes[size].width};
    `}
    `;
    // 스타일컴포넌트
    const StyledButton = styled.button`
    
    /*공통스타일*/
    display:inline-flex;
    outline: none;
    border:none;
    border-radius:4px;
    color:white;
    font-weight:bold;
    cursor:pointer;
    padding: 1em;
    justify-contents:center;
    

    /*크기*/
    font-size:1em;
    margin: 0.5em;
    align-items:center;
    
    /*색상*/
    ${colorStyle}   
    /*크기스타일*/
    ${sizestyle}
    //전체너비 100%스타일
    ${props=>{
        return props.fullWidth &&
        css `
        width:100%; 
        `;
    }}
   
    &+&{
        margin-left: 1em;
    }
    `;
    
    return (
        <StyledButton color={color} size={size} fullWidth={fullWidth} {...res}>{children}</StyledButton>
    );
};

ButtonTotal.defaultProps={
    color:'blue',
    size:'medium'
}
export default ButtonTotal;

코드를 보면 스타일컴포넌트를 만들기 전에 변수에 css를 저장하고 css가 저장된 변수를 스타일 컴포넌트에 넣어서 사용해 준다. ${colorStyle} , ${sizestyle}

 

꼭 이렇게 코드를 작성하는 게 더 좋다는 건 아니고 이러면 보기에 조금 깔끔해 보이기는 하는데

내 개인적인 생각으로는 스타일 컴포넌트 자체가 모든 스타일을 컴포넌트 안에 정의해 줘서 코드를 간결하게 해 주는데 이렇게 밖에서 만들어서 적용해 주면 저 변수에 어떤 게 들어가는지 또 찾아가서 봐야 하고 좀 '굳이?'라는 생각이 들긴 한다. 취향인 쪽을 쓰거나 취업을 한다면 사수가 원하는 방향으로 써주면 되겠지... 그래도 나는 안에 다 들어있는 게 좋다고 생각함 왔다 갔다 하는 거 진짜 너무 힘들다.. 컴포넌트도 많아서 걔네들 돌아가면서 보는 것도 힘든데..

 

Polished 라이브러리-- wooah(우아한..)

Polished

저번에 sass와 styled-component를 설치할 때처럼 사용하려는 프로젝트 폴더로 이동해서 설치해 준다

npm install --save polished

 

이제 polish에 있는 기능을 import 해서 사용해 주면 된다. 여기서는 lighten이라는 기능을 사용해 줄 것이다.

https://polished.js.org/docs/#lighten

 

✨ polished | Documentation

A lightweight toolset for writing styles in JavaScript.

polished.js.org

스타일컴포넌트 아래에 있던 

&:hover {
      background: #1c7ed6;
}

요기를 지워주고 

colorstyle css에 같이 넣어주겠다.

import { lighten } from 'polished';

const ButtonTotal = ({children,color,size,fullWidth,...res}) => {
    //배경색변수
    const colorStyle=css`
    ${({theme,color})=>{
        const selected = theme.palette[color];
        return css`
        background: ${selected};
        &:hover {
            background:${lighten(0.2,selected)}
        }
        `;
    }}`;

여기 완성코드의 위쪽 부분을 잘라온 건데 lighten을 import 해주고 colorStyle을 만들 때 색을 받아오기 때문에 

colorstyle안에다 적어주고 지정된 색이 들어있는selected를 같이 사용해 주고 밝기를 조절해 준다

 

 

이제 여기에 삭제 버튼을 박스 안에 하나  추가해 주고 삭제버튼을 추가하면  대화창이 나오게 해 주겠다. 

 

App.js

import "./styles.css";
import styled, { ThemeProvider } from "styled-components";
import ButtonTotal from "./ButtonTotal";


const AppBlock = styled.div`
  width: 512px;
  margin: 0 auto;
  margin-top: 4em;
  border: 1px solid black;
  padding: 1em;
`;

export default function App() {
  return (
    <ThemeProvider
      theme={{
        palette: {
          blue: "#228be6",
          gray: "#495057",
          pink: "#f06595"
        }
      }}
    >
      <AppBlock>
        <div>
          <ButtonTotal size="large">Button</ButtonTotal>
          <ButtonTotal>Button</ButtonTotal>
          <ButtonTotal size="small">Button</ButtonTotal>
        </div>
        <div>
          <ButtonTotal size="large" color="pink">
            Button
          </ButtonTotal>
          <ButtonTotal color="pink">Button</ButtonTotal>
          <ButtonTotal color="pink" size="small">
            Button
          </ButtonTotal>
        </div>
        <div>
          <ButtonTotal size="large" color="gray">
            Button
          </ButtonTotal>
          <ButtonTotal color="gray">Button</ButtonTotal>
          <ButtonTotal color="gray" size="small">
            Button
          </ButtonTotal>
        </div>
        <div>
          <ButtonTotal fullWidth>Button</ButtonTotal>
        </div>
        <div>
          <ButtonTotal fullWidth color="pink">
            삭제
          </ButtonTotal>
        </div>
      </AppBlock>
    </ThemeProvider>
  );
}

ButtonTotal컴포넌트를 하나 더 넣고 children으로 "삭제"를 넣어준다

버튼을 하나 추가

이제 저 삭제 버튼을 누르면 화면이 어두워지고 대화창? 같은 게 뜨도록 해주겠다.

Dialog.js

import React from 'react';
import styled from 'styled-components';
import ButtonTotal from './ButtonTotal';

//배경 컴포넌트
const DarkBackground = styled.div`
    position:fixed;
    left: 0;
    top:0;
    width: 100%;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items:center;
    background: rgba(0,0,0,0.8);
`;
// 컨펌창 블럭
const DialogBlock = styled.div`
    width:320px;
    padding: 1.5em;
    background: white;
    border-radius: 2px;
    h3{ //DialogBlock안에 있는 h3태그에적용
        margin: 0;
        font-size: 1.5em;
    }
    p{ //DialogBlock안에 있는 p태그에 적용
        font-size: 1.125em;
    }
`;
const Dialog = ({title,children,confirmText,cancelText}) => {
    
    return (
        <DarkBackground>
            <DialogBlock>
                <h3>{title}</h3>
                <p>{children}</p>
                <div>
                    <ButtonTotal color="gray">{confirmText}</ButtonTotal>
                    <ButtonTotal color="pink"}>{cancelText}</ButtonTotal>
                </div>
            </DialogBlock>
        </DarkBackground>
    );
};

export default Dialog;

 어두운 배경이 될 DarkBackground컴포넌트를 만들고 그 안에

확인창이 될 DialogBlock 컴포넌트를 만들어서 넣어줬다.

app.js에서 props로  {title, children, confirmText, cancelText}를 받아와서 내용으로 넣어준다.

ButtonTotal 컴포넌트를 import 해서 버튼을 만들어준다.

 

App.js

import "./styles.css";
import styled, { ThemeProvider } from "styled-components";
import ButtonTotal from "./ButtonTotal";
import Dialog from "./Dialog";

const AppBlock = styled.div`
  width: 512px;
  margin: 0 auto;
  margin-top: 4em;
  border: 1px solid black;
  padding: 1em;
`;

export default function App() {
  return (
    <ThemeProvider
      theme={{
        palette: {
          blue: "#228be6",
          gray: "#495057",
          pink: "#f06595"
        }
      }}
    >
      <AppBlock>
        <div>
          <ButtonTotal size="large">Button</ButtonTotal>
          <ButtonTotal>Button</ButtonTotal>
          <ButtonTotal size="small">Button</ButtonTotal>
        </div>
        <div>
          <ButtonTotal size="large" color="pink">
            Button
          </ButtonTotal>
          <ButtonTotal color="pink">Button</ButtonTotal>
          <ButtonTotal color="pink" size="small">
            Button
          </ButtonTotal>
        </div>
        <div>
          <ButtonTotal size="large" color="gray">
            Button
          </ButtonTotal>
          <ButtonTotal color="gray">Button</ButtonTotal>
          <ButtonTotal color="gray" size="small">
            Button
          </ButtonTotal>
        </div>
        <div>
          <ButtonTotal fullWidth>Button</ButtonTotal>
        </div>
        <div>
          <ButtonTotal fullWidth color="pink">
            삭제
          </ButtonTotal>
        </div>
      </AppBlock>
      <Dialog
        title="정말로 삭제하시겠습니까"
        confirmText="삭제"
        cancelText="취소"
      >
        데이터를 정말로 삭제하시겠습니까
      </Dialog>
    </ThemeProvider>
  );
}

Dialog 컴포넌트도 내부에서 쓰고 있는 ButtonTotal컴포넌트에서 ThemeProvider값을 쓰고 있기 때문에

안쪽에 마운트 해준다.

Dialog에 props로 title , confirmText , cacelText  , 그리고 태그 사이에 있는 값인 children을 전송해 준다.

 

지금은 아무것도 누르지 않았는데 Dialog가 마운트 되어서 보이게 된다. 이걸 삭제버튼을 클릭하면 mount가 보이도록 설정하고 삭제나 취소를 누르면 다시 사라지도록 해주겠다.

 

App.js 

import "./styles.css";
import styled, { ThemeProvider } from "styled-components";
import ButtonTotal from "./ButtonTotal";
import Dialog from "./Dialog";
import {useState} from 'react';

const AppBlock = styled.div`
  width: 512px;
  margin: 0 auto;
  margin-top: 4em;
  border: 1px solid black;
  padding: 1em;
`;

export default function App() {
  const[dialog,setDialog]=useState(false); //dialog를 보이고 안보이게 해줄 상태값 false일때 안보임
  function onClick(){
    setDialog(true); //함수실행되면 true로 바뀌면서 dialog가 보이게 해줌
  }
  const onConfirm = ()=>{
    console.log('확인') 
    //확인 버튼을 누르면 false가 되고 dialog가 사라짐
    setDialog(false)
  }
  const onCancel = ()=>{
    console.log('취소') 
    //취소 버튼을 누르면 false가 되고 dialog가 사라짐
    setDialog(false)
  }
  return (
    <ThemeProvider
      theme={{
        palette: {
          blue: "#228be6",
          gray: "#495057",
          pink: "#f06595"
        }
      }}
    >
      <AppBlock>
        <div>
          <ButtonTotal size="large">Button</ButtonTotal>
          <ButtonTotal>Button</ButtonTotal>
          <ButtonTotal size="small">Button</ButtonTotal>
        </div>
        <div>
          <ButtonTotal size="large" color="pink">
            Button
          </ButtonTotal>
          <ButtonTotal color="pink">Button</ButtonTotal>
          <ButtonTotal color="pink" size="small">
            Button
          </ButtonTotal>
        </div>
        <div>
          <ButtonTotal size="large" color="gray">
            Button
          </ButtonTotal>
          <ButtonTotal color="gray">Button</ButtonTotal>
          <ButtonTotal color="gray" size="small">
            Button
          </ButtonTotal>
        </div>
        <div>
          <ButtonTotal fullWidth>Button</ButtonTotal>
        </div>
        <div>
          <ButtonTotal onClick={onClick} fullWidth color="pink">
            삭제
          </ButtonTotal>
        </div>
      </AppBlock>
      <Dialog
        title="정말로 삭제하시겠습니까"
        confirmText="삭제"
        cancelText="취소"
        dialog={dialog}
        onConfirm={onConfirm}
        onCancel={onCancel}
      >
        데이터를 정말로 삭제하시겠습니까
      </Dialog>
    </ThemeProvider>
  );
}

App에서 Dialog를 보이거나 보이지 않게 해 줄 상태값을 만들고 그 값을 변경하는 함수를 만들어서 Dialog에  props로 보내준다 onClick 함수는 삭제 버튼이 클릭되면 실행되어야 하기 때문에 ButtonTotal컴포넌트에 넣어준다.

ButtonTotal.js

import React from "react";
import styled, { css } from "styled-components";
import { lighten } from "polished";

// 스타일컴포넌트
const StyledButton = styled.button`
    
/*공통스타일*/
display:inline-flex;
outline: none;
border:none;
border-radius:4px;
color:white;
font-weight:bold;
cursor:pointer;
padding: 1em;
justify-contents:center;

/*크기*/
font-size:1em;
margin: 0.5em;
align-items:center;

/*색상*/
${(props) => props.colorstyle}   
/*크기스타일*/
${(props) => props.sizestyle}   
//전체너비 100%스타일
${(props) => {
  return (
    props.fullWidth &&
    css`
      width: 100%;
    `
  );
}}

&+&{
    margin-left: 1em;
}
`;

const ButtonTotal = ({ children, color, size, fullWidth, ...res }) => {
  //배경색변수
  const colorstyle = css`
    ${({ theme, color }) => {
      const selected = theme.palette[color];
      return css`
        background: ${selected};
        &:hover {
          background: ${lighten(0.2, selected)};
        }
      `;
    }}
  `;
  // 사이즈
  const sizes = {
    large: {
      height: "3em",
      fontSize: "1.25em",
      width: "40%"
    },
    medium: {
      height: "2.5em",
      fontSize: "1em",
      width: "25%"
    },
    small: {
      height: "1.75em",
      fontSize: "1em",
      width: "15%"
    }
  };
  const sizestyle = css`
    ${({ size }) => css`
      height: ${sizes[size].height};
      font-size: ${sizes[size].fontSize};
      width: ${sizes[size].width};
    `}
  `;

  return (
    <StyledButton
      color={color}
      size={size}
      fullWidth={fullWidth}
      colorstyle={colorstyle}
      sizestyle={sizestyle}
      {...res}
    >
      {children}
    </StyledButton>
  );
};

ButtonTotal.defaultProps = {
  color: "blue",
  size: "medium"
};
export default ButtonTotal;

ButtonTotal컴포넌트에서는 보내주는 onClick을 받아야 하기 때문에... res로 받고 스타일 버튼에도 다시 Props를 {... res}로 넘겨준다

Dialog.js

import React from "react";
import styled from "styled-components";
import ButtonTotal from "./ButtonTotal";

//배경 컴포넌트
const DarkBackground = styled.div`
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background: rgba(0, 0, 0, 0.8);
`;
// 컨펌창 블럭
const DialogBlock = styled.div`
  width: 320px;
  padding: 1.5em;
  background: white;
  border-radius: 2px;
  h3 {
    margin: 0;
    font-size: 1.5em;
  }
  p {
    font-size: 1.125em;
  }
`;
const Dialog = ({
  title,
  children,
  confirmText,
  cancelText,
  dialog,
  onCancel,
  onConfirm
}) => {
  if (!dialog) return null; //dialog가 false면 null을 리턴하고 아래의 리턴은 읽지않는다.
  return (
    <DarkBackground>
      <DialogBlock>
        <h3>{title}</h3>
        <p>{children}</p>
        <div>
          <ButtonTotal color="gray" onClick={onConfirm}>
            {confirmText}
          </ButtonTotal>
          <ButtonTotal color="pink" onClick={onCancel}>
            {cancelText}
          </ButtonTotal>
        </div>
      </DialogBlock>
    </DarkBackground>
  );
};

export default Dialog;

App에서 보내준 상태값과 상태값을 바꾸는 함수들을 Dialog에서 받아서 함수는 버튼에 클릭이벤트로 넣고 dialog는 

조건문으로 dialog가 true일 때는 return null 이 되게 조건을 준다. 그러면 dialog에서 null을 return 해주기 때문에 결과적으로 아무것도 보이지 않는다.

 

삭제버튼을 클릭하면 dialog가 뜨고 확인이나 취소를 클릭하면 dialog가 사라지는 걸 확인할 수 있다.

 

이 제거의 다 왔다.. dialog가 생기고 없어질 때 지연시간을 주고 생길 때는 아래에서 올라오게 해주고 사라질 때는 아래로 내려가는 애니메이션 효과를 주겠다. 지연시간을 주려면 처음에 생길때는 크게 상관없지만 사라질때는 useEffect를 이용해서 값이 변경될 때 애니메이션을 실행시켜 주고 지연시간뒤에 애니메이션을 지워준뒤에 dialog가 사라지도록 해줘야 한다..

나도 아직 완전히 이해한 게 아니라서 최대한 풀어서 설명해도 이 정도가 나의 최선... 

 

바로 시작!

Dialog.js

styled-components로 애니메이션만들기

styled-components에서 keyframes를 import해준다음 애니메이션을 만들어준다.

styled-componets에 애니메이션적용

컴포넌트 안에 animation이름을 넣어주고 애니메이션실행시간(animation-duration) 애니메이션 끝난 후상태(animation-fill-mode)를 설정해 줬다 지금은 컴포넌트가 생길 때 실행될 애니메이션만 추가되어 있고 사라질 때 실행될 애니메이션도 추가해 주겠다.

 

Dialog에서 스타일컴포넌트에 disappear라는 props를 보내주고 disappear가 true이면 아래의 css코드가 적용되면서 윗줄에 있는 animation-name을 덮어써준다.

props.disappear

이렇게 disappear props를 전달해 주는데 dialog가 false가 될 때(창이 사라질 때) → disappear는 true가 되면서 slideDown, fadeOut 애니메이션이 실행되게 해 준다.

 

이렇게 하면 실행될 것 같지만 실행되지 않는다

애니메이션이 실행되기 전에 이미 return null이 돼서 사라져 있기 때문..

그래서 애니메이션을 실행한 후에 사라지도록 조건과 값을 주겠다.

조건을 주기 위해 Dialog컴포넌트 안에 상태값(State)을 2개 만들어줬다  animate , localdialog

animate

dialog가 false가 돼서 사라질 때(사라지는 애니메이션 실행) true가 되고

애니메이션의 끝나는 시간인(animation-duration) 0.5초 뒤에 false가 되게 해 준다.

(사라지는 애니메이션 지속시간 동안만 true가 되는 상태값)

 

localdialog 

지금은 확인이나 취소버튼을 클릭해서 dialog가 false로 바뀌면 바로 return null이 되는데

바로 return null이 되지 않게 해줘야 한다.

localdialogdialog값(true)을 넣어준다 dialog가 false로 바뀌고 localdialog가 true일 때는

return null을 하지 않고

animate상태값을 true로 만들어주고 사라지는 애니메이션을 0.5초 실행 후 animate값을 false로 바꿔준다 

그 후에 localdialog값(true)도 dialog값(false)과 똑같이 만들어주고

만약localdialog와 animate 가 둘 다 false라면 null을 리턴해준다. 

if(localdialog && !dialog){ //확인이나취소버튼을 클릭해서 dialog가 false로 set된 시점
      setAnimate(true); //사라지는 애니메이션실행
      setTimeout(()=>setAnimate(false),500); //사라지는 애니메이션 종료시점 0.5초 뒤
}
setlocaldialog(dialog); //로컬dialog은false가 됨
if(!animate && !localdialog) return null; //fadeout,slideDown애니메이션이 끝나고 localdialog가 false면 null을 리턴

이런 조건문이 나오는데 이게 실행되어야 하는 시점이 dialog값이 변하는 시점에 실행시켜 주면 된다.

값이 변하는 시점에 실행시켜주는 hook으로 useEffect를 사용해 준다

useEffect(()=>{
    //dialog값이 true에서 false로 바뀔때를확인. (확인이나취소버튼클릭하면)
    if(localdialog && !dialog){
        setAnimate(true); //fadeout,slideDown 애니메이션 실행중~
        setTimeout(()=>setAnimate(false),500); //fadeout,slideDown 애니메이션 실행끝~
    }
    setlocaldialog(dialog); //localdialog는false가 됨
},[localdialog,dialog]) //localdialog,dialog값이 변하는시점에 실행
if(!animate && !localdialog) return null; //fadeout,slideDown애니메이션이 끝나고 localdialog가 false면 null을 리턴

이렇게 길고 긴 styled-component 프로젝트가 끝났다.. 마지막에 useEffect가 압권이긴 했는데 리액트에 점점 익숙해져서 조금씩 이해가 되는 것 같아서 기분은 좋다,,

확실히 코드는 처음에는 이해가 안돼도 천천히 읽어보고 다시 만들어보면 뇌에 스며들게 되는 것 같다.. 물론 안 보고 똑같이는 아직 못 만들겠지만.. (사실 이것도 과제로 내주면 검색해 가면서 어떻게든 만들겠지..)

반응형

+ Recent posts