결과물

 

 

 

 

 

 

Detail.jsx

디테일 페이지에서 값을 수정하려고 한다. 내가 생각했을 땐 입력 상태를 업데이트 해주는 각 setState가 필요할 것 같았는데, useRef로 수정 기능을 많이 구현한다고 했다. 

 

useRef()를 사용해서 초기값을 먼저 설정해준다. 텍스트 수정은 input 태그에 defaultValue에 담아 데이터를 불러올 수 있다.

const dateRef = useRef(exe.date);
const itemRef = useRef(exe.item);
const descRef = useRef(exe.desc);
const amountRef = useRef(exe.amount);
<label>날짜</label>
<StInput ref={dateRef} defaultValue={exe.date} />
<label>항목</label>
<StInput ref={itemRef} defaultValue={exe.item} />
<label>내용</label>
<StInput ref={descRef} defaultValue={exe.desc} />
<label>금액</label>
<StInput ref={amountRef} defaultValue={exe.amount} />

 

 

 

 

 

 

수정할 데이터 접근

변수명.current.value로 수정할 데이터에 접근할 수 있다. 그래서 따로 수정한 데이터를 담은 객체를 생성해주었다. 

const modifiedData = {
    id: exe.id,
    date: dateRef.current.value,
    item: itemRef.current.value,
    amount: +amountRef.current.value,
    desc: descRef.current.value,
};

 

해당 월 지출내역이 있는 배열에 순회를 돌려서 해당 월 지출 내역의 id 값이 내가 수정하려는 지출 내역과 같다면 수정된 데이터가 저장되도록 했다. 여기서 계속 오류가 났었는데 이유는 아래와 같다.

 

콘솔에서 찍어보면 수정은 잘되는데 기존에 있던 데이터가 삭제되고 내가 수정한 객체가 새로운 배열로 반환되어 수정하려는 데이터와 수정한 데이터 두개만 찍히는 것이다. 

 

 

오류가 나타난 이유는 바로 map 함수를 사용하면서 해당 객체에 접근할 수 있는 매개변수를 ex로 설정해놓고 exe로 써놓았던 것.. 아직 map 함수를 정확히 이해하지 못한 것 같다^^.. 이렇게 바꾸고 다시 수정을 시도했더니 아주 잘 됐다.. .. 

const modifyExes = data[exe.month].map((ex) =>
      ex.id === id ? modifiedData : ex
);

 

 

 

👾 오류

여기서 잘되나 싶었는데 수정하고 나서 홈화면으로 나갔다가 수정했던 데이터를 다시 수정하려고 하니까 아래 오류가 나왔다. 수정 함수를 건드렸는데 onRemove() 함수에 있는 filter가 안되는 것이다. data[exe.month]를 확인해보니 undefined가 떴다. 

Uncaught TypeError: Cannot read properties of undefined (reading 'filter')

 

콘솔에 수정된 데이터 변수인 modifyExes를 찍어보니 month 값이 안찍히고 있었던 것. 그래서 data[exe.month] 찍었을 때 데이터를 불러오지 못하고 undefined가 떴구나. 

 

 

✨ 해결

수정 코드에 스프레드 연산자를 사용했다. 기존 객체에는 month가 있고, 새로 만든 수정 객체에는 month가 없기 때문에 스프레드 연산자로 합쳐주었다.

더보기

✏️ 스프레드 연산자

기존 배열이나 객체의 전체 또는 일부를 다른 배열이나 객체로 빠르게 복사할 수 있습니다. 기존에 생성되어 변수에 할당되어 있는 배열이나 객체를 새로운 변수에 할당 하게되면 새로운 변수는 기존의 변수에 할당되어 있는 객체 또는 배열을 참조하게 됩니다. 객체에서는 spread 연산자를 이용하여 객체의 프로퍼티를 업데이트 하거나 복사할 수 있습니다.

좌변엔 원본 데이터를 복사하고, 우변엔 덮어 씌울 데이터를 복사한다. 여기서 중복되는 키값은 우변으로 덮어씌워지고 새로운 프로퍼티가 있는 경우 업데이트되게 된다. 아래의 예시를 보면 이해가 쉽다.

const person1 = { name: "john", age:"18" }
const person2 = { name: "Alice", age:"20", citiy:"seoul" }

const sum = {...person1, ...person2}
//결과 : { name: "Alice", age:"20", citiy:"seoul" }

 

그렇게 바꿔준 코드이다. 콘솔에 찍어보니 month도 잘찍히는 것을 확인할 수 있다.

const modifyExes = data[exe.month].map((ex) =>
      ex.id === id ? { ...ex, ...modifiedData } : ex
);

 

 

수정한 데이터를 다시 저장해주고, 로컬스토리지에 저장해주면 완성! 수정기능을 하면서 아직 map, filter 배열과 객체를 제대로 알지 못하는 것 같았다. 잘안다고 생각했는데 코드에 활용할 때 이론이 백지가 되버린다. 제대로 더 공부해봐야겠다.

data[exe.month] = modifyExes;
localStorage.setItem("expenseList", JSON.stringify(data));

 

 

 

결과물

아래 이미지와 같이 월을 눌렀을 때 해당하는 월의 지출 내역을 보여주도록 구현해보려고 한다. 로컬스토리지에도 데이터를 저장하고 삭제하는 것까지가 목표다.

 

 

 

 

데이터 추가

우선 expenseList 변수명으로 데이터가 있다면 저장된 데이터들을 객체 형태로 바꿔서 불러오고, 없다면 빈 객체를 가져온다. 객체로 불러오는 이유는 월별마다 다른 지출내역이 보여야하기 때문에 월을 key 값으로 value를 해당 월의 지출 내역 리스트로 하기 위함이다.

const initalLocalData = localStorage.getItem("expenseList")
    ? JSON.parse(localStorage.getItem("expenseList"))
    : {};

 

새로운 객체를 입력받는 값으로 month:selectedMonth 를 넣어주어 추가할 때마다 해당하는 월이 어딘지 확인해준다. 그리고 데이터를 입력받은 후 기존 객체를 풀어 addExes 새로운 객체에 넣어준다. 

const onInsert = (date, item, amount, desc) => {
    const newExes = {
      id: uuidv4(),
      date,
      item,
      amount,
      desc,
      month: selectedMonth, 
    };

    //객체로 가져오기
    //key - 월, value - 지출내역
    const addExes = {
      ...exes,
    };
  };

 

해당하는 월에 지출내역이 없다면? 빈배열을 보여줘 그리고 해당하는 월에 새로운 데이터를 배열에 push 해줘! selectedMonth를 배열에 담은 이유는 월마다 다른 지출내역을 배열에 넣어주기 위함이다. push가 가능한 이유도 배열이기 때문!

if (!addExes[selectedMonth]) {
      addExes[selectedMonth] = [];
    }
    
    addExes[selectedMonth].push(newExes);

 

 

데이터를 추가하고 상태를 업데이트해주고, 로컬스토리지에 string 타입으로 저장해준다. 불러올때는 꼭 객체 타입으로 변환해서 가져오자!

setExes(addExes);
localStorage.setItem("expenseList", JSON.stringify(addExes));

 

해당 월 지출 내역을 담은 변수를 선언해주고 이 변수를 지출 리스트 컴포넌트에 props로 내려준다. 그럼 지출 리스트 컴포넌트에서 해당 데이터를 갖고 map을 돌려 화면에 데이터를 보여준다.

const filteredList = exes[selectedMonth];

 

 

 

 

ExesList.jsx

 

지출 내역 리스트를 화면에 뿌려주는 컴포넌트이다. 필러팅된 props를 map으로 돌려준다. 근데 여기서 👾오류 발생.. 오류 메세지는 아래와 같다.. 정의되지 않은 속성은 읽을 수 없다고 한다.. 저기서 대체 왜?.. 데이터들도 배열로 잘 뜨고 있는데 왜 뜰까..

Uncaught TypeError: Cannot read properties of undefined (reading 'map')
{filteredList.map((exe) => {
      return <ExesItem key={exe.id} exe={exe} setExes={setExes} />;
})}

 

👾 오류 이유

렌더링이 화면에 커밋 된 후에 모든 효과를 실행하기 때문이다. 즉 return에서 map을 반복 실행할 때 첫 턴에 데이터가 아직 다 안들어와도 렌더링이 실행되며, 당연히 그 데이터는 undefined로 정의되어 오류가 나는 것이다. 그렇게 한참을 구글링하다가 나랑 같은 오류를 해결한 글을 보았다. 

 

 

✨ 해결 방법

논리곱연산자 &&를 사용하자! 자바스크립트에서 true && expression은 항상 expression으로 실행되고  false && expression은 항상 false로 실행된다. 따라서 조건이 참이면 && 바로 뒤의 요소가 출력에 나타난다. 거짓이면 무시하고 건너뛴다.

{filteredList && filteredList.map((exe) => {
	return <ExesItem key={exe.id} exe={exe} setExes={setExes} />;
})}

 

 

 

여기까지해주면 월별로 들어가는 지출 내역이 잘 저장되는 것을 확인할 수 있다.

 

 

 

 

 

 

 

 

데이터 삭제

삭제는 디테일 페이지에서 진행해보자. 각 지출 리스트를 눌렀을 때 해당하는 리스트의 id 값으로 상세페이지로 들어간다. useParams()를 사용해서 파라미터를 가지고온다.

 const { id } = useParams();

 

 

useNavigate()로 페이지를 이동하면서 기존 객체 상태를 props로 가지고 오고, 디테일 페이지에선 useLocation()으로 사용해줬다. 여태 컴포넌트로 만들어서 부모 -> 자식으로 props를 넘겨주는 방식으로만 했는데 라우터를 배우면서 상세페이지를 만들고, 페이지 간에 props를 전달하고 사용할 수 있다는 점은 이번 프로젝트를 진행하면서 처음 알았다. 

const navigate = useNavigate();
  const onDetailButtonHandler = (id) => {
    navigate(`/detail/${id}`, { state: { exe } });
  };

 

Detail.jsx

const location = useLocation(); 
const item = location.state.exe;

 

 

 

받은 props를 사용해 삭제 기능을 구현해보자. 내 페이지는 부모-자식 컴포넌트가 아니라 종속성이 없기때문에 로컬스토리지 데이터를 삭제해주도록 했다. data라는 변수에 저장된 로컬스토리지 데이터들을 담아주고 console.log를 찍어주면 데이터는 잘 불러온다.

let data = JSON.parse(localStorage.getItem("expenseList"));

 

 

 

삭제 함수를 만들어 데이터를 삭제해보자. id 값을 받아와 기존 객체 배열에서 id가 일치하지 않는 값들을 모두 removeData 변수에 담아준다. id 값이 같은 데이터를 제외한 나머지 값 = removeData 라고 생각하면 된다. 그리고 할당된 값을 다시 data[item.month]에 다시 담아준다. 이건 삭제된 데이터를 제외한 나머지 데이터를 재할당(갱신)하는 것이라고 생각하면 된다. 삭제된 값을 제외하고 나머지 값을 다시 로컬스토리지에 저장해야하기 때문이다.

const onRemove = (id) => {
    const removeData = data[item.month].filter((exe) => exe.id !== id);
    data[item.month] = removeData;
  };

 

여기서 filter를 돌릴 때 data[item.month]로 돌리는 이유는 위 로컬스토리지에서 불러온 데이터를 담은 변수 data는 객체로 불러오기 때문이다. filter 메서드는 배열에만 사용할 수 있기때문에 data[item.month]로 해당하는 월의 데이터로 filter를 돌리면 된다.

아래는 콘솔로 찍어본 data와 data[item.month]이다. 객체와 배열 형태로 찍히는 것을 확인할 수 있다.

 

 

그렇게 재할당해준 데이터를 다시 로컬스토리지에 저장해주면 삭제하기 완성..! 여기서 navigate(-1) 은 뒤로가기이다. 상세페이지에서 데이터를 삭제하고 새로고침을 하거나 홈으로 나가야 삭제가 됐는지 확인할 수 있었기 때문에 삭제와 동시에 홈으로 이동하게끔 했다. 

localStorage.setItem("expenseList", JSON.stringify(data));
navigate(-1);

 

뒤로가기 버튼도 마찬가지! -1을 해준다 원래는 navigate('/')로 했었는데 이건 덮어씌우는거랑 같다고 해서 대부분 -1을 사용한다고한다.

const onBackButtonHandler = () => {
    navigate(-1); 
  };

 

 

 

 

 

삭제 기능까지 완성! 혼자 하는데 처음에 월별로 데이터를 저장하면서 월을 key값으로 객체로 저장을 하려니 잘쓰고 있던 map, filter 메서드들에 오류가 나서 다 백지상태였다. 배열 메서드들인데 내가 객체로 저장해버리니 오류가 나는 수밖에 튜터님한테 많이 물어보고 콘솔에 객체와 배열이 어떻게 다른지 물어보면서 만들어보았다. 아직 복습을 많이 해야할 것 같다.  

 

 

 

저장

로컬스토리지에 저장하는 코드는 아래와 같다. 나는 useState()를 사용하고 있어서 defendency arry에 useState 변수를 넣어주어 상태가 변할 때마다 실행하도록 했다. 객체나 배열을 저장해줄 때에는 JSON.stringify를 이용해서 객체를 string으로 변환시켜준 후에 저장해주어야 한다. 

useEffect(() => {
    localStorage.setItem("expenseList", JSON.stringify(exes));
  }, [exes]);

 

 

 

조회

로컬스토리지에서 불러오는 코드는 아래와 같다. 저장할 때 사용한 변수명으로 불러올 수 있다.  JSON.stringify로 객체를 string으로 변환해준 것을 다시 객체로 변환해주기 위해 JSON.parse를 이용한다.

const initalLocalData = localStorage.getItem("expenseList")
    ? JSON.parse(localStorage.getItem("expenseList"))
    : [];

 

 

 

삭제

삭제하는 코드는 아래와 같다.

localStorage.removeItem('변수명')

 

 


 

 

 

🔥 실제 데이터로 활용해보기

가계부 프로젝트 중 로컬스토리지를 사용해서 웹 서버에 데이터를 저장하려고 한다. 우선 로컬스토리지에 데이터가 있는지 확인하고 있다면 가져오고, 없다면 빈 배열을 가져오도록 해준다. 그리고 useEffect를 사용해 defendency array에 상태가 변할 때마다 업데이트될 수 있도록 해주자.

const initalLocalData = localStorage.getItem("expenseList")
    ? JSON.parse(localStorage.getItem("expenseList"))
    : [];
// exes가 변경될 때마다 로컬 스토리지에 업데이트.
  useEffect(() => {
    localStorage.setItem("expenseList", JSON.stringify(exes));
  }, [exes]);

 

아래 이미지처럼 input 창에 데이터를 입력하면 위에 데이터가 등록이 된다. 입력할 때 로컬스토리지에 저장되어야하기 때문에 추가해주는 컴포넌트에 가서 코드를 추가해주자.

 

 

 

 

지출 내역을 추가하는 컴포넌트로 와서 코드를 추가해줬다. 객체나 배열을 저장해줄시에는 JSON.stringify를 이용해서 string으로 변환시켜준 후에 저장한다. JSON.stringify없이 저장해둔 데이터는 [Object Object]와 같이 저장된다.

 

아래와 같이 추가가 되었다! 데이터도 저장된 걸 확인할 수 있다.

 

 

 

 

리액트 숙련주차에 들어갔다. 입문 주차에서는 무엇인가 새로운 것을 배운다는 느낌에 집중도 잘되었는데 숙련 주차에 들어오니 끊임없는 정보때문에 머리가 헤롱하다. 과부하가 온 것 같다. 지금 배우고 있는 단계가 실무에서 가장 많이 쓰고 있는 것이라고 한다. 이럴때일수록 더 집중하고 이해해보려고 노력해야겠다.🔥

 


 

 

 

Styled-components

css-in-js 란?

자바스크립트 코드로 css 코드를 작성하여 컴포넌트를 꾸미는 방식이다. css를 사용할 때 조건문과 변수 등 다양한 로직을 적용해서 사용할 수 있다는 장점이 있다.

 

 

패키지 설치

yarn add styled-components

 

사용방법

태그의 속성명에 접근하여 스타일 주는 방법은 아래와 같다.

import styled from "styled-components";

styled.button`
  // <button> HTML 엘리먼트에 대한 스타일 정의
`;

 

컴포넌트에 접근하여 스타일 주는 방법은 아래와 같다.

import styled from "styled-components";
import Button from "./Button";

styled(Button)`
  // <Button> React 컴포넌트에 스타일 정의
`;

 

태그 속성에 원하는 스타일을 주고 StyledButton 변수에 저장한다. 해당 변수를 Button 컴포넌트에 사용하면 다른 리액트 컴포넌트에서 같은 스타일이 적용된 <Button>을 사용할 수 있다.

import React from "react";
import styled from "styled-components";

const StyledButton = styled.button`
  padding: 6px 12px;
  border-radius: 8px;
  font-size: 1rem;
  line-height: 1.5;
  border: 1px solid lightgray;
  color: gray;
  background: white;
`;

function Button({ children }) {
  return <StyledButton>{children}</StyledButton>;
}
import Button from "./Button";
<Button>Default Button</Button>;

 

 

 

 

 

 

 

useState()

 

기본 문법

컴포넌트의 state를 관리할 수 있는 기본적은 hook 이다. state를 변수로 사용하고, setState를 이용해서 state 값을 수정할 수 있다.

const [state, setState] = useState(initialState);

 

 

함수형 업데이트

카운트로 예시를 들어보자. 기존에 우리가 카운트 값을 증가시켰던 방법은 원시적 데이터로 값을 올려주었다. ( ) 안에 수정할 값이 아니라 함수를 넣어보자. 

import { useState } from "react";

export default function useStatePage() {
  const [count, setCount] = useState(0);
  
  const onPlusHandler = () =>{
  	setCount(count + 1);
  }

  return (
    <div>
      <span> 카운트 {count}시 </span>
      <button onClick={()=>onPlusHandler}>+</button>
    </div>
  );
}

 

( ) 안에 현재 number의 값을 가져와서 그 값에 +1을 더하여 반환하는 함수를 만들었다. 일반적인 업데이트 방식와 함수 업데이트 방식은 어떤 점이 다른지 아래에서 확인해보자.

setCount((currentNumber)=>{ return currentNumber + 1 });

 

 

일반 업데이트 방식은 버튼을 클릭했을 때 첫번째 줄 ~ 세번째 줄의 있는 setCount가 각각 실행되는 것이 아니라, 배치(batch)로 처리한다. 즉 우리가 onClick을 했을 때 setCount 라는 명령을 세번 내리지만, 리액트는 그 명령을 하나로 모아 최종적으로 한번만 실행을 시킨다는 뜻이다. 그래서 setCount을 3번 명령하던, 100번 명령하던 1번만 실행한다.

<button onClick={() => {
  	setCount(count + 1); // 첫번째 줄 
  	setCount(count + 1); // 두번쨰 줄
  	setCount(count + 1); // 세번째 줄
}}
>

 

반면에 함수형 업데이트 방식은 3번을 동시에 명령을 내리면, 그 명령을 모아 순차적으로 각각 1번씩 실행시킨다. 현재값 0에 1더하고, 그 다음 1에 1을 더하고, 2에 1을 더해서 3이라는 결과가 나오는 것을 확인할 수 있다.

<button onClick={() => {
          setCount((previousState) => previousState + 1);
          setCount((previousState) => previousState + 1);
          setCount((previousState) => previousState + 1);
        }}
>

 

 

 

예시

자식 컴포넌트에 버튼을 눌렀을 때 부모 컴포넌트에 카운트가 증가하도록 하는 코드를 구현해보자.

import { useState } from 'react';

const App = () => {
	const [count, setCount] = useState(0);

	return (
		<div>
			<h1>여기는 부모컴포넌트입니다.</h1>
			<span>현재 카운트 : {count}</span>
			<Child setCount={setCount}/> 
		</div>
	)
}

export default App;

 

Child 컴포넌트에 setCount만 props로 넘겨주면 된다. 함수형으로 값을 업데이트해줄 것이기때문에 count 까지 넘겨주어 setCount(count + 1) 을 해주는 것은 불필요한 props 전달이기때문이다. 

<Child setCount={setCount}/>
const Child = (props) => {
	const handleAddCound = () => {
		setCount((prev) => prev + 1);
	}

	return <button oncClick={handleAddCound}>Count 1 증가</button>
}

export default Child;

 

 

 

 

 

useEffect()

 

기본 문법

리액트 컴포넌트가 렌더링 된 이후마다 특정 작업을 수행하도록 설정할 수 있는 Hook이다. 컴포넌트가 마운트 / 언마운트 / 업데이트 됐을 때 특정 작업을 처리할 수 있다.

useEffect(setup, dependencies?)

 

 

useEffect는 컴포넌트가 렌더링된 이후에 실행된다는 것이 핵심 포인트이다. 아래와 같은 코드는 input 태그를 만들어 state와 연결해준 상태이다. input창에 값을 입력했을 때 state가 변경하므로 리렌더링이 되면서 console.log 값이 계속해서 찍히는 것을 확인할 수 있다. 

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

const App = () => {
  const [value, setValue] = useState("");

  useEffect(() => {
    console.log("hello useEffect");
  });

  return (
    <div>
      <input
        type="text"
        value={value}
        onChange={(event) => {
          setValue(event.target.value);
		      console.log("value => ", value);
        }}
      />
    </div>
  );
}

export default App;

 

1. 인풋 값 입력 > 2. state 변경 > 3. 리렌더링 > 4. 렌더링이 되었으니 useEffect 다시 실행 > 반복 과 같은 흐름이다.

 

 

 

의존성 배열(defendency array)

 

Hook이 불필요하게 반복해서 실행되는 것을 방지하여 성능을 최적화하기 위해서 사용한다.

// useEffect의 두번째 인자가 의존성 배열이 들어가는 곳
useEffect(()=>{
	// 실행하고 싶은 함수
}, [의존성배열]) //[] 이 배열에 값을 넣으면 그 값이 바뀔 때만 useEffect를 실행할게

✔️ Hook의 재실행 조건 설정

- 의존성 배열에 포함된 값들이 변경될 때만 훅이 재실행되도록 조건을 설정한다. 이를 통해 불필요한 실행을 방지할 수 있다.

 

✔️ 최신 상태 유지

- 의존성 배열을 사용하면 Hook이 항상 최신 상태의 값을 참조할 수 있다. 이를 통해 불필요한 연산을 방지할 수 있다.

 

✔️ 의존성 관리의 명확성

- 의존성 배열을 사용하면 훅이 어떤 값에 의존하고 있는지 명확하게 파악할 수 있다. 이는 코드의 가독성을 높이고, 의도하지 않은 의존성 관계를 방지할 수 있다.


 

 

의존성 배열이 비어있는 경우는, 렌더링된 이후 딱 한번만 실행하겠다는 뜻이다. 렌더링 이후 한번만 실행하고 싶다면 빈 배열을 넣어주면 된다. 

// src/App.js

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

const App = () => {
  const [value, setValue] = useState("");
  useEffect(() => {
    console.log("hello useEffect");
  }, []); // 비어있는 의존성 배열

  return (
    <div>
      <input
        type="text"
        value={value}
        onChange={(event) => {
          setValue(event.target.value);
        }}
      />
    </div>
  );
}

export default App;

 

 

반대로 의존성 배열이 채워져 있는 경우에는, 이 배열의 값이 변경될 때만 실행하겠다는 뜻이다. 인풋의 값이 바뀔 때마다 실행한다.

// src/App.js

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

const App = () => {
  const [value, setValue] = useState("");
  useEffect(() => {
    console.log("hello useEffect");
  }, [value]); // value를 넣음

  return (
    <div>
      <input
        type="text"
        value={value}
        onChange={(event) => {
          setValue(event.target.value);
        }}
      />
    </div>
  );
}

export default App;

 

 

 

클린 업(Clean up)

컴포넌트가 언마운트 즉, 사라졌을 때 실행하는 함수이다. 

useEffect(()=>{
	// 화면에 컴포넌트가 나타났을(mount) 때 실행하고자 하는 함수를 넣어주세요.

	return ()=>{
	// 화면에서 컴포넌트가 사라졌을(unmount) 때 실행하고자 하는 함수를 넣어주세요.
	}
}, [])

 

 

 

 

 

 

 

useRef()

 

기본 문법

리렌더링과 상관없이 값을 기억하기 위해 사용된다. 이 특징을 이용해 자바스크립트 DOM API를 직접 사용하지 않고 DOM 요소를 다루기 위한 용도로도 자주 사용된다. 

const 변수명 = useRef(초기값)

 

 

 

useRef()에 초기값을 주고 실행해보면 current : '초기값' 을 확인할 수 있다. 이는 current라는 키값을 지닌 프로퍼티가 생성되고, 값에 어떤 변경을 줄때도 이 current를 이용해서 한다는 점이다.

import "./App.css";
import { useRef } from "react";

function App() {
  const ref = useRef("초기값");
  console.log("ref 1", ref);

  return <div></div>;
}

export default App;

 

아래 코드와 같이 ref.current 의 값을 바꾼 값으로 변경하고 실행해보면 다음과 같다.

import "./App.css";
import { useRef } from "react";

function App() {
  const ref = useRef("초기값");
  console.log("ref 1", ref);

  ref.current = "바꾼 값";
  console.log("ref 1", ref);

  return <div></div>;
}

export default App;

 

이렇게 설정된 ref 값은 컴포넌트가 계속해서 렌더링 되어도 언마운트 전까지 값을 유지하며 currnet 속성은 값을 변경해도 상태를 변경할 때 처럼 리액트 컴포넌트가 리렌더링 되지 않는다는 특징이 있다. 

 

 

 

 

저장공간

state와 같이 어떤 값을 저장하는 공간으로 사용한다. 대신 state는 리렌더링이 필요한 값을 다룰때 사용하고, ref는 변하지 않는 값을 다룰 때 사용하자. 변화는 감지하지만 그 변화가 렌더링을 발생시키지 않을 때! 사용하기!

Hook rendering change
useState() Y 내부 변수 초기화
useRef() N 값 유지

 

 

 

 

 

 

 

 

 

 

https://ejunyang.tistory.com/entry/PP-%EB%A6%AC%EC%95%A1%ED%8A%B8%EB%A1%9C-ToDoList-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EC%82%AD%EC%A0%9C-%EA%B8%B0%EB%8A%A5

 

[P.P] 리액트로 ToDoList 만들기 - 삭제 기능

https://ejunyang.tistory.com/entry/PP-%EB%A6%AC%EC%95%A1%ED%8A%B8%EB%A1%9C-ToDoList-%EB%A7%8C%EB%93%A4%EA%B8%B0 [P.P] 리액트로 ToDoList 만들기 - UI구현, 리스트 추가또 다른 프로젝트가 시작됐다 ㅎ 저번주 금요일에 리액트

ejunyang.tistory.com

 

 

 

UI 구현

  • Todo 추가 하기
  • Todo 삭제 하기
  • Todo 완료/취소 상태 변경하기 (진행중 ↔ 완료)

 

기능 구현

  • 제목과 내용을 입력하고, [추가하기] 버튼을 클릭하면 Working에 새로운 Todo가 추가되고 제목 input과 내용 input은 다시 빈 값으로 바뀌도록 구성해주세요.
  • [삭제하기] 버튼을 클릭하면 Working 또는 Done 에 있는 것과 상관없이 삭제처리가 되도록 해주세요.
  • Todo의 isDone 상태가 true이면, 상태 버튼의 라벨을 취소, isDone이 false 이면 라벨을 완료 로 조건부 렌더링 해주세요.
  • Todo의 상태가 Working 이면 위쪽에 위치하고, Done이면 아래쪽에 위치하도록 구현합니다.
  • Layout의 최대 너비는 1200px, 최소 너비는 800px로 제한하고, 전체 화면의 가운데로 정렬해주세요.
  • 컴포넌트 구조는 자유롭게 구현해보세요.

 

 

🍯 HINT

  • 사용한 hook은 오직 useState
  • 기능 구현을 위해 생성한 함수는 2개 입니다. onChangeHandler , onSubmitHandler
  • 사용한 javascript 내장 메서드는 map, filter 입니다.
  • todo의 initial state는 {id: 0, title: “”, body: “”, isDone: false} 입니다.

 


 

 

컴포넌트

TodoTemplate.jsx  화면을 가운데에 정렬시켜주며, 일정 리스트를 보여줍니다. children으로 내부 JSX를 props로 받아 와서 렌더링해줍니다.
TodoAdd.jsx 할 일을 추가하는 컴포넌트 입니다.
TodoList.jsx todos 배열을 props로 받아 온 후, 이를 배열 내장 함수 map을 사용해서 여러 개의 TodoItem 컴포넌트로 변환하여 보여 줍니다.
TodoItem.jsx 진행중인 할 일 정보를 보여주는 컴포넌트입니다. todo 객체를 props로 받아 와서 상태에 따라 다른 스타일의 UI를 보여줍니다
CurrentDate.jsx 현재 날짜를 알려주는 컴포넌트입니다.
Icons.jsx 배경에 꾸며주는 Sticker 요소를 보여주는 컴포넌트입니다.

 


 

 

App.jsx

Todo의 isDone 상태가 true이면, 상태 버튼의 라벨을 취소, isDone이 false 이면 라벨을 완료 로 조건부 렌더링이 필요해서 토글 함수를 만들었다. 삭제 함수와 마찬가지로 id 값을 받아와서 처리하도록 했다.

 

map과 삼항연산자를 활용했다. map()으로 배열 순회를 돌면서 배열의 id와 사용자가 선택한 id 가 같다면, todo에 isDone 키의 값이 true -> false, false -> true 로 반전시켜주는 NOT 연산자를 사용해줬다. 같지 않다면 그대로 todo가 나오도록 해주었다.

const onToggle = (id) => {
    setTodo(
      todos.map((todo) =>
        todo.id === id ? { ...todo, isDone: !todo.isDone } : todo
      )
    );
  };

 

TodoList 에서 사용할 수 있게 props로 내려준다. 그리고 진행중인 리스트와 진행 완료된 리스트를 나누어서 완료된 리스트는 진행 중인 리스트 아래로 내려야 하기때문에 TodoList 를 두개 만들어주고 안에 들어가는 props 에 filter를 써서 조건부 렌더링이 되도록 해주었다.

const workingTodo = todos.filter((todo) => !todo.isDone);
const doneTodo = todos.filter((todo) => todo.isDone);
return (
    <>
      <TodoTemplate>
        <CurrentDate todos={todos} />
        <TodoList
          key={todos.id}
          todos={workingTodo}
          onRemove={onRemove}
          onToggle={onToggle}
        />
        <TodoList
          key={todos.id}
          todos={doneTodo}
          onRemove={onRemove}
          onToggle={onToggle}
        />
        <TodoAdd onInsert={onInsert} />
      </TodoTemplate>
    </>
  );

 

 

 

TodoList.jsx

TodoItem에서 사용할 수 있도록 props를 내려주자.

const TodoList = ({ todos, onRemove, onToggle }) => {
  return (
    <div>
      {todos.map((todo) => {
        return (
          <TodoItem
            todo={todo}
            key={todo.id}
            onRemove={onRemove}
            onToggle={onToggle}
          />
        );
      })}
    </div>
  );
};

 

 

 

TodoItem.jsx

isDone 이 true 일때 체크 아이콘은 ✔️ 상태를 유지하고, isDone 이 false 라면 O 체크를 지운 아이콘을 유지하도록 했다.

{isDone && (<IoCheckmark className="check" onClick={() => onToggle(id)} />)}
{!isDone && (<PiCircleLight className="check" onClick={() => onToggle(id)} />)}

 

이렇게 코드를 적고 실행을 했는데 오류가 생겼다. isDone is not defined. 

 

코드를 확인해보니 구조할당분해에서 isDone을 빼먹어서 생긴 오류였다. isDone 을 넣어주니 정상적으로 작동하였다.

const { id, contents, isDone } = todo;

 

 

 

https://ejunyang.tistory.com/entry/PP-%EB%A6%AC%EC%95%A1%ED%8A%B8%EB%A1%9C-ToDoList-%EB%A7%8C%EB%93%A4%EA%B8%B0

 

[P.P] 리액트로 ToDoList 만들기 - UI구현, 리스트 추가

또 다른 프로젝트가 시작됐다 ㅎ 저번주 금요일에 리액트 강의가 발제 됐는데 자바스크립트 보다 훨씬 재밌고, 뭔가 이해가 더 잘 되는것 같다(?) 자바스크립트 라이브러리 아닌가..? 강의 듣는

ejunyang.tistory.com

 

 

 

UI 구현

  • Todo 추가 하기
  • Todo 삭제 하기
  • Todo 완료/취소 상태 변경하기 (진행중 ↔ 완료)

 

기능 구현

  • 제목과 내용을 입력하고, [추가하기] 버튼을 클릭하면 Working에 새로운 Todo가 추가되고 제목 input과 내용 input은 다시 빈 값으로 바뀌도록 구성해주세요.
  • [삭제하기] 버튼을 클릭하면 Working 또는 Done 에 있는 것과 상관없이 삭제처리가 되도록 해주세요.
  • Todo의 isDone 상태가 true이면, 상태 버튼의 라벨을 취소, isDone이 false 이면 라벨을 완료 로 조건부 렌더링 해주세요.
  • Todo의 상태가 Working 이면 위쪽에 위치하고, Done이면 아래쪽에 위치하도록 구현합니다.
  • Layout의 최대 너비는 1200px, 최소 너비는 800px로 제한하고, 전체 화면의 가운데로 정렬해주세요.
  • 컴포넌트 구조는 자유롭게 구현해보세요.

 

 

🍯 HINT

  • 사용한 hook은 오직 useState
  • 기능 구현을 위해 생성한 함수는 2개 입니다. onChangeHandler , onSubmitHandler
  • 사용한 javascript 내장 메서드는 map, filter 입니다.
  • todo의 initial state는 {id: 0, title: “”, body: “”, isDone: false} 입니다.

 


 

컴포넌트

TodoTemplate.jsx  화면을 가운데에 정렬시켜주며, 일정 리스트를 보여줍니다. children으로 내부 JSX를 props로 받아 와서 렌더링해줍니다.
TodoAdd.jsx 할 일을 추가하는 컴포넌트 입니다.
TodoList.jsx todos 배열을 props로 받아 온 후, 이를 배열 내장 함수 map을 사용해서 여러 개의 TodoItem 컴포넌트로 변환하여 보여 줍니다.
TodoItem.jsx 진행중인 할 일 정보를 보여주는 컴포넌트입니다. todo 객체를 props로 받아 와서 상태에 따라 다른 스타일의 UI를 보여줍니다
CurrentDate.jsx 현재 날짜를 알려주는 컴포넌트입니다.
Icons.jsx 배경에 꾸며주는 Sticker 요소를 보여주는 컴포넌트입니다.

 


 

 

 

App.jsx

filter 메서드를 사용해서 배열을 순회하도록 했고, 배열에 있는 id 와 사용자가 선택한 id 가 일치한 값은 삭제가 되고, 일치하지 않는 나머지 값들을 리턴해주도록 했다. 여기서 느슨한 비교와 엄격한 비교를 할 수 있는데 비교 방법은 아래와 같다.

 // 삭제기능
  const onRemove = (id) => {
    setTodo(todos.filter((todo) => todo.id !== id));
  };

 

만들어준 onRemove()는 TodoList 컴포넌트에서 사용할 수 있도록 props 로 내려주자.

return (
    <>
      <TodoTemplate>
        <CurrentDate todos={todos} />
        <TodoList
          key={todos.id}
          todos={todos}
          onRemove={onRemove}
        />
        <TodoAdd onInsert={onInsert} />
      </TodoTemplate>
    </>
  );

 


🐢 느슨한 비교

자체적으로 타입변환이 필요한 경우에만 사용하자.

// 느슨한 비교
console.log(5 == '5'); // true
console.log(0 == false); // true
console.log(null == undefined); // true

 

🔥 엄격한 비교

// 엄격한 비교
console.log(5 === '5'); // false
console.log(0 === false); // false
console.log(null === undefined); // false
 // 느슨한 비교
  const onRemove = (id) => {
    setTodo(todos.filter((todo) => todo.id != id));
  };
  
 // 엄격한 비교
  const onRemove = (id) => {
    setTodo(todos.filter((todo) => todo.id !== id));
  };

 

 

TodoList.jsx

import React from "react";
import TodoItem from "./TodoItem";

const TodoList = ({ todos, onRemove }) => {
  return (
    <div>
      {todos.map((todo) => {
        return (
          <TodoItem
            todo={todo}
            key={todo.id}
            onRemove={onRemove}
          />
        );
      })}
    </div>
  );
};

export default TodoList;

 

TodoItem.jsx

 

내려받은 props 를 고대로 사용만 하면 되는 TodoItem에서 마지막 작업을 해주면 된다. 버튼을 클릭할 때 함수가 실행되도록 해주면 삭제하기 버튼 완료이다. 나는 버튼 모양 대신에 내가 만든 휴지통 아이콘을 넣고싶어서 따로 import해주고 사용봤다.

import React from "react";
import deleteIcon from "../assets/delete.png";
import { IoCheckmark } from "react-icons/io5";
import { PiCircleLight } from "react-icons/pi";

const TodoItem = ({ todo, onRemove }) => {
  const { id, contents, isDone } = todo;
  return (
    <>
      <div className="todoItem">
        <p className="content">{contents}</p>
        <button onClick={() => onRemove(id)}>
          <img src={deleteIcon} />
        </button>
      </div>
    </>
  );
};

export default TodoItem;

 

삭제하기 버튼 생성 완료.. 후후... 기능 구현도 아주 잘 된다.. 후후..

 

 

 

또 다른 프로젝트가 시작됐다 ㅎ 저번주 금요일에 리액트 강의가 발제 됐는데 자바스크립트 보다 훨씬 재밌고, 뭔가 이해가 더 잘 되는것 같다(?) 자바스크립트 라이브러리 아닌가..? 강의 듣는데 이해가 잘되서 재밌던 적은 처음이었다 이게 얼마나 갈까?ㅎㅎ

 

 


 

 

UI 구현

  • Todo 추가 하기
  • Todo 삭제 하기
  • Todo 완료/취소 상태 변경하기 (진행중 ↔ 완료)

 

기능 구현

  • 제목과 내용을 입력하고, [추가하기] 버튼을 클릭하면 Working에 새로운 Todo가 추가되고 제목 input과 내용 input은 다시 빈 값으로 바뀌도록 구성해주세요.
  • [삭제하기] 버튼을 클릭하면 Working 또는 Done 에 있는 것과 상관없이 삭제처리가 되도록 해주세요.
  • Todo의 isDone 상태가 true이면, 상태 버튼의 라벨을 취소, isDone이 false 이면 라벨을 완료 로 조건부 렌더링 해주세요.
  • Todo의 상태가 Working 이면 위쪽에 위치하고, Done이면 아래쪽에 위치하도록 구현합니다.
  • Layout의 최대 너비는 1200px, 최소 너비는 800px로 제한하고, 전체 화면의 가운데로 정렬해주세요.
  • 컴포넌트 구조는 자유롭게 구현해보세요.

 

 

🍯 HINT

  • 사용한 hook은 오직 useState
  • 기능 구현을 위해 생성한 함수는 2개 입니다. onChangeHandler , onSubmitHandler
  • 사용한 javascript 내장 메서드는 map, filter 입니다.
  • todo의 initial state는 {id: 0, title: “”, body: “”, isDone: false} 입니다.

 

 


 

 

 

와이어프레임

컴포넌트

TodoTemplate.jsx  화면을 가운데에 정렬시켜주며, 일정 리스트를 보여줍니다. children으로 내부 JSX를 props로 받아 와서 렌더링해줍니다.
TodoAdd.jsx 할 일을 추가하는 컴포넌트 입니다.
TodoList.jsx todos 배열을 props로 받아 온 후, 이를 배열 내장 함수 map을 사용해서 여러 개의 TodoItem 컴포넌트로 변환하여 보여 줍니다.
TodoItem.jsx 진행중인 할 일 정보를 보여주는 컴포넌트입니다. todo 객체를 props로 받아 와서 상태에 따라 다른 스타일의 UI를 보여줍니다
CurrentDate.jsx 현재 날짜를 알려주는 컴포넌트입니다.
Icons.jsx 배경에 꾸며주는 Sticker 요소를 보여주는 컴포넌트입니다.

 

 

 

 

🐣 프로젝트 생성

1. CRA(Create React App)

아래처럼 성공 메세지가 띄워졌다면 app 생성 성공

yarn create react-app [원하는 프로젝트 이름]

 

 

생성한 App 파일로 Change Directory 하고 yarn을 실행해준다. 그럼 리액트 앱 생성 완료~~ EZEZ~~

cd [생성한 App 이름]
yarn start

 

 

 

2. Vite

yarn create vite [생성할 App 이름] --template react

 

 

생성 완료닷

cd [생성한 App 이름]
yarn
yarn dev

 

Vite로 생성하면 좋은 이유

WebPack을 사용하는 CRA 대신 Esbuild를 사용하는 Vite는 원래 Vue.js 애플리케이션을 위해 만들어졌지만, 
현재는 React, Svelte, Vanilla JS 등 다양한 프레임워크와 라이브러리를 지원한다. 

1. 빠른 콜드 스타트와 HMR(Hot Module Replacement)
2. 속도 측면에서 기존 CRA와는 비교가 되지 않을 정도로 빠르다
3. CRA는 기본적으로 설정을 숨기지만, Vite는 사용자가 필요에 따라 설정을 더 쉽게 조정할 수 있다
4. Go 언어 베이스의 자바스크립트 빌드 툴입니다. CRA가 채택하는 웹팩과 비교할 때, 말이 안되는 수준의 속도

 

 

 

 

 

🐥 UI 구현하기

우선 App.jsx에 리스트 배열을 만들어주고 각 컴포넌트를 배치한다. TodoList가 두개인 것은 진행 중인 리스트 목록과 완료한 리스트 목록을 UI로 구분하기 위함이다.

import React, { useState } from "react";
import "./App.css";
import TodoTemplate from "./component/TodoTemplate";
import TodoAdd from "./component/TodoAdd";
import CurrentDate from "./component/CurrentDate";
import TodoList from "./component/TodoList";

function App() {
  const [todos, setTodo] = useState([
    {
      id: Date.now(),
      contents: "주 2회 클라이밍 가기",
      isDone: false,
    },
    {
      id: Date.now() + 1,
      contents: "식단 관리하기",
      isDone: false,
    },
  ]);


  return (
    <>
      <TodoTemplate>
        <CurrentDate/>
        <TodoList/>
        <TodoList/>
        <TodoAdd/>
      </TodoTemplate>
    </>
  );
}

export default App;
import React, { useState } from "react";
import "./App.css";
import TodoTemplate from "./component/TodoTemplate";
import TodoAdd from "./component/TodoAdd";
import CurrentDate from "./component/CurrentDate";
import TodoList from "./component/TodoList";

 

컴포넌트를 앞으로도 잘 이용할것이라면 import하는 것을 잊지말자. 하면서도 import 오류가 발생하면 아차 싶은 1인..

 

TodoTemplate.jsx

템플릿은 이렇게 구현해주었다. children 으로 내부 JSX를 가져와서 템플릿 안 어떤 컨텐츠든 자유자재로 바뀌도록 해준다. 

import React from 'react'
import Icons from './Icons'

const TodoTemplate = ({children}) => {
  return (
    <div className='template'>
      <div className='wrap'>
        <Icons/>
        {children}
        </div>
    </div>
  )
}

export default TodoTemplate

 

html로 생각해보면 아래와 같다.

<div class = "template">
	<div class = "wrap"></div>
</div>

 

 

Icons.jsx

import React from 'react'
import icon1 from '../assets/icon1.png';
import icon2 from '../assets/icon2.png';
import icon3 from '../assets/icon3.png';
import icon4 from '../assets/icon4.png';

const Icons = () => {
    return (
        <div className='icons'>
            <img src={icon1} alt='todolist' />
            <img src={icon2} alt='icon-heart' />
            <img src={icon3} alt='icon-music' />
            <img src={icon4} alt='icon-sticker' />
        </div>
    )
}

export default Icons

 

 

 

CurrentDate.jsx

현재 날짜를 생성해주고 today에 할당해주었다. 나는 [ 요일, 일자 월 ] 이렇게 배치할거라 요일과 월은 따로 배열에 넣어주었다. 월, 요일 배열 선언을 마쳤다면 포뱃변수에 노출될 날짜를 할당해주면 된다.

import React from 'react'

const CurrentDate = ({todos}) => {
  const today = new Date();
  const month = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
  const week = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
  let curMonth = month[today.getMonth()];
  let dayWeek = week[today.getDay()];
  const formatDate = `${dayWeek}, ${today.getDate()} ${curMonth}`

  const taskCount = todos.length;

  return (
    <div>
        <h2 className='curdate'>{formatDate}</h2>
        <p className='taskCount'>{taskCount} tasks</p>
    </div>
  )
}

export default CurrentDate

 

 

 

🍯 리액트에서 아이콘 사용하는 방법

아래와 같다. 1️⃣ 명령어 입력 -> 2️⃣ React Icons에 들어간다 -> 3️⃣ 원하는 아이콘 검색 후 import

yarn add react-icons // yarn

https://react-icons.github.io/react-icons/

 

React Icons

 

react-icons.github.io

 

 

 

✨ TodoAdd.jsx

contents로 받은 값으로 새로운 객체를 생성해 기존 객체에 추가해준다. 여기서 스프레드 연산자를 사용한 이유는 불변성을 유지하기 위해서이다. 배열이니 push로 추가해도되지 않을까 할 수 있지만 push한다면 배열에 직접적으로 추가하는 것이기 때문에 값은 변할지 몰라도 메모리 주소는 바뀌지 않는다. 배열 중간에 것을 삭제하면 오류가 날 수 있고, 이것은 불변성 유지가 불가능하다고 볼 수 있다.

 

그래서 스프레드 연산자로 기존 객체를 복사해주고 그 뒤로 생성한 객체를 배열에 넣어주었다. 이러면 불변성을 유지할 수 있다.

// 할 일 추가
  const onInsert = (contents) => {
    const newTodo = {
      id: Date.now(),
      contents: contents,
    };

    setTodo([...todos, newTodo]);
  };

 

onInsert 함수를 만들어주었으니 추가 컴포넌트에서 사용할 수 있도록 props로 받아오자.

<TodoAdd onInsert={onInsert} />

 

추가 컴포넌트에서 기능 구현을 위해 사용한 리액트 Hook은 useState(), useRef(), useEffect()이다. 차례대로 이해한대로 정리를 해보면 아래와 같다.

 

✅ useSatae() : 컴포넌트의 상태를 관리해준다. 가변하는 사용자 입력 값을 동적으로 사용하기 위함.

✅ useRef() : current라는 키값을 지닌 프로퍼티가 생성되고, 값에 어떤 변경을 줄때도 current를 이용한다.
DOM요소에 접근이 가능하면, 불필요한 재렌더링을 하지 않는다는 장점이 있다.

✅ useEffect() : 컴포넌트가 렌더링될때마다 특정 작업을 실행 할 수 있다. (처음 시작할때 / 사라질때 / 업데이트 될때

 

 

 

 

우선, useState로 상태를 선언해준다. 초기값은 빈 문자열로 해준다. 위에 새로운 객체를 추가하는 함수 onInsert를 만들때 contents 값을 받아와 객체를 생성해주도록 했기때문에 onInsert(contents=사용자로부터 받아온 값) 으로 넣어준다. 생성할때 onClick이 아니라 onSubmit를 사용해준 이유는 form 태그를 사용했기 때문이다. 🍯 onSubmit 은 form 태그 내부에 이벤트를 실행할 수 있다. form 제출 이벤트가 발생할 때의 동작을 지정한다. 

const TodoAdd = ({ onInsert }) => {
    const [contents, setContents] = useState('');

    const onChange = (e) => {
        setContents(e.target.value);
    }

    const onSubmit = (e) => {
        onInsert(contents);
        setContents(''); //빈칸
    }


return (
        <>
            <div className='inserPosition'>
                <form className='insertForm' onSubmit={onSubmit}>
                    <p className='modalTitle'>Today</p>
                    <input type='text' value={contents} placeholder='할 일을 입력하고 Enter를 눌러주세요.' onChange={onChange} />
                </form>
            </div>

            <button style={addBtnstyle}}>
                <HiOutlinePlus style={{ color: '#fff', margin: '0' }} />
            </button>
        </>
    )
}

 

이때 오류가 났었는데 console.log로 확인했을 땐 값이 잘 들어가는데 새로고침되면서 값이 안보이는 오류가 생겼다. 이럴때에는 e.preventDefault(); 를 사용해주면 된다.preventDefault 를 통해 이러한 동작을 막을 수 있다.

e.preventDefault();

 

 

추가로 나는 + 버튼을 눌렀을 때 input창을 띄우는 작업을 하고 싶었다. 감이 안와서 튜터님께 여쭤보니 하고싶은 기능을 말로 해보라고 하셨다. "+ 버튼을 눌렀을 때, 인풋창을 띄우고 싶어요."  말 그대로다 처음에 useState로 인풋창의 상태를 false로 지정해주었다.

const [open, setOpen] = useState(false); //인풋창 띄우기(현재 false)

 

그리고 버튼을 눌렀을 때를 코드로 작성해보면 아래와 같다. 난 왜 응용을 못할까.. 🤬 말 그대로 버튼을 눌렀을 때 부정 연산자를 사용해서 상태를 반전(true - false, false - true) 해주는 것이다. 버튼에서 open 상태를 사용할 수 있게 속성으로 가지고 왔다.

<button style={addBtnstyle} onClick={() => setOpen(!open)} open={open}>
<HiOutlinePlus style={{ color: '#fff', margin: '0' }} />
</button>

 

이제 +를 누르면 인풋창이 뜨겠지? 했지만 오류.. 왜그런가 했더니 open 상태가 true가 되었을 때 어떤 화면이 렌더링 되는지 안정해줬기 때문이다. inserPosition 이 클래스를 띄우면 되는데 어찌할까 구글링하다 논리곱연산자로 구현할 수 있는 방법을 찾았다. 

논리곱연산자는(&&) 좌변 true일 때 우변을 실행한다. 아래 코드를 해석해보자면 속성에 open이 true 이면 <div> 태그를 실행해줘~ 다. 

return (
        <>
        {open && (
            <div className='inserPosition'>
                <form className='insertForm' onSubmit={onSubmit}>
                    <p className='modalTitle'>Today</p>
                    <input type='text' ref={inputEl} value={contents} placeholder='할 일을 입력하고 Enter를 눌러주세요.' onChange={onChange} />
                </form>
            </div>
        )}
            <button 
            style={addBtnstyle} 
            className={`rotateBtn ${open ? 'rotated' : ''}`} 
            onClick={() => setOpen(!open)} 
            open={open}>
                <HiOutlinePlus style={{ color: '#fff', margin: '0' }} />
            </button>
        </>
    )

 

실행해보면 오류 없이 잘 나타나는 것을 확인 할 수 있다. 

 

진짜 진짜 마지막으로 +버튼을 누르고 할일을 바로 치려는데 자동 커서가 안되어있어서 인풋창에 커서를 눌렀다가 써야한다는 불편함이 있었다. 그래서 열심히 구글링 다시 시작. 자동커서를 구현하기 위해선 다들 useRef 훅을 사용하는 것 같았다.

const 변수명 = useRef(초기값)

 

userRef를 먼저 생성해주고, input창에 속성으로 ref값을 설정해준다. 

const inputEl = useRef(null);
<input type='text' 
ref={inputEl} 
value={contents} 
placeholder='할 일을 입력하고 Enter를 눌러주세요.' 
onChange={onChange} />

 

버튼을 클릭했을때 input element가 focus 되어야하기 때문에 useEffect를 사용하였다. 이것도 구글링을 통해서 알게된 훅이다. 그러면 랜더링될 때마다 input 엘리먼트가 focus를 유지하는 걸 확인 할 수 있다.

useEffect(() => {
       if(inputEl.current !== null) inputEl.current.focus();  //자동커서
})

 

 


 

UI + 추가기능 완성본

 

 

 

 

 

 

🔗 참조 사이트

 

https://xiubindev.tistory.com/100

 

React Hooks : useEffect() 함수

useEffect 함수는 리액트 컴포넌트가 렌더링 될 때마다 특정 작업을 실행할 수 있도록 하는 Hook 이다. useEffect는 component가 mount 됐을 때, component가 unmount 됐을 때, component가 update 됐을 때, 특정 작업을

xiubindev.tistory.com

https://programming119.tistory.com/100

 

[JS] event.preventDefault() 간단 설명 😊/ preventDefault란?

preventDefault 란? a 태그나 submit 태그는 누르게 되면 href 를 통해 이동하거나 , 창이 새로고침하여 실행됩니다. preventDefault 를 통해 이러한 동작을 막아줄 수 있습니다. 주로 사용되는 경우는 1. a 태

programming119.tistory.com

+ 강의자료

 

📌  콜백함수?

 

어느 함수에 인자로 넘겨주는 함수로 어떤 이벤트가 발생했거나 특정 시점에 도달했을 때 시스템에서 호출하는 함수이다. 쉽게 설명해서 다른 함수의 실행이 끝나고 실행하는 것이 콜백함수이다. forEach , setTimeout 등이 있다.

콜백함수는 다른 코드에게 인자로 넘겨줌으로써 제어권도 함께 위임한 함수이다. 콜백 함수를 위임 받은 코드는 자체적으로 내부 로직에 의해 콜백함수를 적절함 지점에서 실행한다.

function print(callback) {
    callback();
}

 

💡 익명함수

 

콜백함수는 일회성으로 사용하는 경우가 많아 코드의 간결성을 위해 익명함수를 사용한다. 함수 내부에서 매개변수로 실행되기 때문에 이름을 붙이지 않아도 되기 때문이다. 함수 이름 충돌을 방지하기 위함도 있다.

 


 

 

 

 

자바스크립트는 코드를 위에서 아래 순으로 실행하지만 순차적으로 실행되지 않을 때도 있다. 이걸 비동기 프로그래밍이라고 한다. 

아래 예시는 0.3초 후라는 적절한 시점에서 자신의 함수에서 적어놓은대로 실행하게된다. 0.3초 뒤 메세지 출력되는걸 확인 할 수 있다.

const message = setInterval(function(){
    console.log('3 second meassage');
    clearInterval(message);
},300);
var cbFunc = function () {
	console.log('3 second meassage');
    clearInterval(message);
};
var timer = setInterval(cbFunc, 300);

 

setTimeout(() => {
    console.log("3 second meassage");
}, 3000);

 

code 호출 주체 제어권
cbFunc() 사용자 사용자
setInterval() setInterval() setInterval()
setTimeout() setTimeout() setTimeout()

 

여기서 cbFunc의 호출 주체와 제어권은 모두 사용자에게 있고 setInterval과 setTimeout의 호출 주체와 제어권은 자기 자신이다.

화살표 함수는 자신만의 this를 가지지 않고 상위 스코프의 this를 참조하기 때문에 전역 객체를 무시하고 무조건 자신을 들고 있는 상위 객체를 가리킨다.

 

 

 

 

this와 콜백함수

이전 this 바인딩 포스팅을 할 때 콜백함수도 찍먹으로 글을 썼었는데 잊혀지지 않은 구문.. 콜백함수도 함수다. 제어권을 넘겨받을 코드에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조한다.

// Array.prototype.map
Array.prototype.map = function (callback, thisArg) {
    var mappedArr = [];
    for (var i = 0; i < this.length; i++) {
        // call의 두 번째 인자는 this가 배열일 것(호출의 주체가 배열)이므로,
        // i번째 요소를 넣어서 인자로 전달
        var mappedValue = callback.call(thisArg || global, this[i]);
        mappedArr[i] = mappedValue;
    }
    return mappedArr;
};
const a = [1, 2, 3].map((item) => {
    return item * 2;
});
console.log(a);

 

여기서 call의 첫 번째 인자는 thisArg로 thisArg가 존재하는 경우는 그 객체, 없으면 전역객체를 가리킨다.

call/apply 메서드의 첫번째 인자에서 콜 백 함수 내부에서 사용될 this를 명시적으로 binding 하기 때문에 this에 다른 값을 넣을 수 있다.

 

 

 

콜백함수에서 this가 전역객체인 이유

콜백 함수는 다른 함수의 인자로 전달되는 함수다. 그래서 콜백 함수는 자신을 전달받은 함수에 의해 호출되는데, 이때 콜백 함수 내부에서의 this는 해당 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않은 경우에는 전역 객체를 참조하게 된다.

 

 

 

콜백함수 this 해결

 

✅ call() : 첫 번째 인자로 this 객체 사용, 나머지 인자들은 , 로 구분

✅ apply() : 첫 번째 인자로 this 객체 사용, 나머지 인자들은 배열 형태로 전달

 

참초할 객체를 추가로 함수의 매개변수로 전달하고, 콜백함수 내에서 call과 apply 메서드를 통해 콜백함수가 참조할 객체를 지정해주면 된다.

var mappedValue = callback.call(thisArg || global, this[i]);

 

 

 

 

이벤트 리스너와 콜백함수

document.queryselector("#callback-btn")
    .addEventListener("click", function() {
      console.log("User has clicked on the button!");
});

 

버튼에 id값을 사용해서 버튼을 선택하고 addEventListener 메서드를 사용하여 이벤트 리스너를 추가했다. 이때 이벤트 리스너 함수는 두개의 파라미터를 필요로 하는데, 첫번째 파라미터로는 이벤트 타입인 '클릭' 이고, 두번째는 버튼이 클릭되었을 때 메세지를 남기는 콜백함 수이다. 콜백함수는 이렇게 이벤트 정의를 위해 사용되기도 한다.

 

 

var obj = {
vals: [1, 2, 3],
logValues: function(v, i) {
console.log(this, v, i);
}
};
//method로써 호출
obj.logValues(1, 2);
//callback => obj를 this로 하는 메서드를 그대로 전달 X
//단지, obj.logValues가 가리키는 함수만 전달
[4, 5, 6].forEach(obj.logValues);

+ Recent posts