Firebase Trigger Email 사용

 

프로젝트 초반부엔 특정 조건하에 E-mail 이 자동 전송될 수 있도록 하는 api 또는, 백엔드를 직접 작성해야 하나 싶었다. 검색에 검색을 더하다보니 Firebase 자체적으로 내장되어 있는 확장 프로그램에 Trigger Email 이 있음을 알게 되었다.

이메일 자동전송을 하는 코드를 직접 작성하는 법을 배워도 좋겠지만, 유지보수 및 적용의 편의성, 데이터 관리의 이점, 마지막으로 Firebase에 어느정도 익숙해진 터라 Trigger-Email 확장프로그램 사용법을 배우기로 했다.

TriggerEmail은 동작 Trigger에 대한 이해만 된다면, 금방 할 수 있지만 공식문서와 정보들이 쥐파먹은 듯이, 어디 한군데에서 내용이 갑자기 비약하던 터라 어떤 방식으로 메일이 전송되는지 파악이 늦어 구현이 시간이 오래 걸렸다.

코드에서는 입력폼을 변경하는 것외엔 특별한 것이 없고, 다만 Firebase / TriggerEmail의 환경설정 부분을 신경써서 작성해주어야 한다.

 

// 기존의 Firebase / FireStore / db 에 addDoc 하는 방식에서부터 Email Trigger의 Trigger 요소를 낑겨넣어준다.   

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    const emailRegex = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/g;

    if (subscriberEmail.match(emailRegex) === null) {
      e.preventDefault();
      alert('이메일을 형식에 맞게 입력해주세요.');
    } else {
      e.preventDefault();
      const subscriberRef = collection(db, 'subscriber');
      const subscriber = {
        to: subscriberEmail,       // E-mail Trigger 에 의해, To 라는 필드에 삽입되는 Email로 자동전송
        name: subscriberName,      // Name이나, SubmitTime과 같은 요소들은 Message 상에 {{name}}, {{ submitTime }} 방식으로 불러올 수 있다. 
        submitTime: submitTime,

        message: {
          subject: '[쟈스민 독립서점 소식지]를 구독해주셔서 감사합니다.',      // message 내에서, subject 항목은 이메일에서 제목으로 발송
          text: '',       // text를 작성해도 좋지만, 이메일을 내용을 이미지로 보내기 위해 아래 HTML 코드로만 작성하였다. 
          html: ' <img src ="https://user-images.githubusercontent.com/78587041/214996034-50d81353-fcc8-48da-8694-7dd2e04dfc40.png" alt ="Thank you for subscribing out Letter" width ="650px" height="1000px"> ',
        },
      };

Input 창에 입력한대로 Message 아래, HTML, Subject, name, submitTime, to 가 잘 들어갔다.

그리고, Trigger-Email 실행에 따라, 아래의 내용이 추가가 되는데, 아래 내용은 에러가 발생했을 경우 볼 수 있다.

 

전송이 성공했을 경우의 메세지. Delivery State 가 Success로 되어 있다.

Firebase의Trigger Email의 환경설정창이다.

 

(선택사항)은 대개 적용이 되어있지않다. jasmine@bookstore.com 으로 작성한다한들 이메일은 발신자의 이메일 주소로 가게 된다.

User Collection 파트는 유저의 이메일 정보가 다른 경로로 수집된 경우 그 안에 [email] 필드가 있으면 따온다.

Templete Collection 같은 경우 Email Template가 여러 개이며 앱 내 이메일 발송 요소가 많을 경우 템플릿을 각 상황에 맞게끔 저장해놓고 알맞게 발송하기 위해 사용한다. 

 

 

 

TypeScript is is a "Superset" to JavaScript

타입스크립트는 자바스크립트를 모두 포함한 기능을 갖춘다.

TypeScript adds static typing typing to JavaScript

타입스크립트는 정적 타이핑을 지원한다.

 

JavaScript on its own is dynamically typed

자바스크립트

는 정적 타이핑을 지원한다.

 

JavaScript는 동적 타이핑을 지원한다의 의미

function add (a,b) {
	return a + b;
}

let result = add (2,5);

console.log(result); // 7 출력

위 코드에서 console.log를 찍었을 때, 7이 나타난다.

function add 에는 자료형이 정해져 있지 않기 때문에

2와 5를 자동으로 숫자형 자료형이라는 것을 알아보고

결과를 도출해내 주기 때문이다. 

function add (a,b) {
	return a + b;
}

let result = add ('2','5');

console.log(result); // 25 출력

위 코드는 함수에서의 변형은 없지만 들어가는 인자가

더이상 숫자형이 아닌, 문자형으로 변경되었다.

하지만 대견스러운 JavaScript는 우리가 의도한 것이

숫자형인지, 문자형인지 알아서 그 나름의 덧셈을 수행해낸다. 

(물론 문자열의 시그널과 숫자열의 시그널을 제공하지만서도..)

 

이 때의 +연산자는 역할이 살짝 바뀌게 된 것인데,

두 매개 변수 2와 5를 더하는 것에서

두 문자열 2와 5를 연결하는 동작을 수행하는 것으로 역할이 살짝 바뀐 것이다. 

 

 

JavaScript는 이미 충분히 괜찮은 언어이지만,

대규모 프로젝트에서는 부족한 점이 있다.

함수나 객체를 의도치 않게 사용하는 일을 예방하기 위해

TypeScript를 사용하게 되는 것이다. 

 

function add (a: number, b:number) {
	return a + b;
}

let result = add('2', '5'); // '2' 와 '5' 아래에 빨간 밑줄이 그인다. 

console.log(result);

위의 코드는 타입스크립트로 작성한 코드인데,

매개변수 a : number라는 조건과

매개변수 b : number 라는 조건이

명확히 주어진다.

 

따라서 문자열인 '2' 와 '5' 에서는 빨간 밑줄이 그이게 되는데

이를 통해, 실행하지 않고도 코드 작성과정에서 문제가 있다는 사실을 인지할 수 있게 한다.

 

이런 이유만으로도 타입스크립트를 사용할 가치는 충분하다고 한다. 

 

 

 

import React, { useEffect } from 'react';
import ShopGuideDetailsComment from '../shopGuideDetailsComment/ShopGuideDetailsComment.jsx';
import { StCommentListContainer } from './ShopGuideDetailsCommentList.js';
import {
  collection,
  getDocs,
  query,
  orderBy,
  // where,
} from 'firebase/firestore';
import { db } from '../../firebase';
import { getAuth } from "firebase/auth";
import { useAuth } from '../../firebase.js';
import { useParams } from 'react-router-dom';



const ShopGuideDetailsCommentList = ({
  // collectionName,
  commentItemtList,
  setCommentItemList,
  comment,
  postingId,
}) => {
  const auth = getAuth();
  const currentUser = auth.currentUser;
  let NavId = useParams();
  const commentCollectionName = NavId.id;

  // 댓글 불러오기 - DB에서 이전 댓글 리스트 불러오기
  const syncCommentListStateWithFirestore = () => {






    const q = query(
      collection(db, 'commentList'),
      // where('postingId', '==', CurrentPostingId),
      !orderBy('savetime', 'desc')
    );

    getDocs(q, currentUser).then((querySnapshot) => {
      const firestoreTodoItemList = [];
      querySnapshot.forEach((doc) => {
        firestoreTodoItemList.push({
          id: doc.id,
          comment: doc.data().comment,
          userId: doc.data().userId,
          savetime: doc.data().savetime,
          modify: doc.data().modify,
          postingId: doc.data().postingId,
          creatorId: doc.data().creatorId,
          commentcreatorid: doc.data().commentcreatorid,
        });
      });
      setCommentItemList(firestoreTodoItemList);
    });
  };

  // useEffect(() => {
  //   syncCommentListStateWithFirestore();
  // }, [commentItemtList]);

  useEffect(() => {
    syncCommentListStateWithFirestore();
  }, []);


  return (
    <StCommentListContainer>
      {commentItemtList.map((item) => {
        if (postingId === item.postingId) {
          return (
            <ShopGuideDetailsComment
              key={item.id}
              item={item}
              commentItemtList={commentItemtList}
              setCommentItemList={setCommentItemList}
              syncCommentListStateWithFirestore={
                syncCommentListStateWithFirestore
              }
            />
          );
        }
      })}
    </StCommentListContainer>
  );
};

export default ShopGuideDetailsCommentList;

 

 

import React, { useState, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import moment from 'moment';
import { modifyModeComment, updateComment } from '../../redux/modules/comment';
import {
  doc,
  deleteDoc,
  updateDoc,
  query,
  getDoc,
  getDocs,
  collection,
  where,
} from 'firebase/firestore';
import { db } from '../../firebase';
import {
  StCommentProfileImage,
  StCommentListContainer,
  StCommentUserName,
  StCommentContentInput,
  StCommentContentSaveTime,
  StCommentContentsEditButton,
  StCommentContentsDeleteButton,
} from './ShopGuideDetailsComment.js';
import { useAuth } from '../../firebase';
import { getAuth } from 'firebase/auth';
import { ref, getDownloadURL, getStorage, listAll } from 'firebase/storage';
import { storage } from '../../firebase.js';

const ShopGuideDetailsComment = ({
  item,
  syncCommentListStateWithFirestore,
  collectionName,
  commentItemtList,
  setCommentItemList,
}) => {
  const time = moment().format('YYYY-MM-DD-hh:mm');
  const { id, comment, savetime, modify } = item;
  const [readOnly, setReadOnly] = useState(true);
  const [updateCommentInput, setUpdateCommentInput] = useState(comment);
  const dispatch = useDispatch();
  //! 민성 수정 
  const [photoURL, setPhotoURL] = useState(``);
  const [commentList, setCommentList] = useState([]);
  const [users, setUsers] = useState([]);
  const storage = getStorage();
  //! 민성 수정
  const auth = getAuth();
  // console.log(auth);
  const currentUser = auth.currentUser;
  // console.log(currentUser);

  // console.log(currentUser.uid);

  // 댓글 수정 -> 완료 모드 토글링 state에 반영하기
  const modifyCommentButtonHandler = (id) => {
    dispatch(modifyModeComment(id));
    setReadOnly(false);
  };

  // 댓글 입력시 - state 반영하기
  const onChangeComment = (event) => {
    const { value } = event.target;
    setUpdateCommentInput(value);
  };

  // 댓글 수정 -> 완료 모드 토글링
  const updateCommentModify = async (id) => {
    const docRef = doc(db, 'commentList', id);
    // console.log(docRef);
    try {
      const response = await updateDoc(docRef, { modify: true });
      console.log(response);
    } catch (event) {
      console.log('error', event);
    } finally {
      console.log('edit mode toggled');
      modifyCommentButtonHandler(id);
    }
    syncCommentListStateWithFirestore();
  };

  // 댓글 수정 완료하기
  const updateCompleteButtonHandler = async (id) => {
    const docRef = doc(db, 'commentList', id);
    try {
      await updateDoc(docRef, {
        modify: false,
        savetime: time,
        comment: updateCommentInput,
      });
      // console.log(response);
    } catch (event) {
      console.log(event);
    } finally {
      console.log('comment updated');
      modifyCommentButtonHandler(id);
      alert('수정이 완료되었습니다.');
    }
    setUpdateCommentInput(updateCommentInput);
    syncCommentListStateWithFirestore();
    setReadOnly(true);
  };

  // 댓글 수정 취소하기
  const editCancelButtonHandler = async (item) => {
    console.log(item);
    const docRef = doc(db, 'commentList', item.id);
    // console.log(docRef.comment);
    // console.log(comment);
    try {
      await updateDoc(docRef, {
        modify: false,
        comment: comment,
      });
      // console.log(response);
    } catch (event) {
      console.log(event);
    } finally {
      console.log('comment update canceled');
      modifyCommentButtonHandler(item.id);
      alert('수정이 취소되었습니다.');
    }
    setUpdateCommentInput(item.comment);
    // setUpdateCommentInput(value);
    // dispatch(modifyModeComment(id));
    syncCommentListStateWithFirestore();
    setReadOnly(true);
  };

  // 댓글 삭제하기
  const deleteCommentButtonHandler = async (removedComment) => {
    console.log(removedComment);

    if (window.confirm('정말 삭제하시겠습니까?')) {
      const commentRef = doc(db, 'commentList', removedComment);
      await deleteDoc(commentRef);
      syncCommentListStateWithFirestore();
    } else {
      return;
    }
  };

  // 닉네임 불러오기

  const syncUserInfoWithFirestore = () => {
    const q = query(
      collection(db, 'users')
      // where('postingId', '==', CurrentPostingId),
      // !orderBy('savetime', 'desc')
    );
    console.log(q);
  };

  useEffect(() => {
    syncCommentListStateWithFirestore();
  }, []);

  // useEffect(() => {
  //   if (!currentUser) return;
  //   // userdeleteCheck();
  // }, [currentUser]);

  //! 민성 수정
  //! db에서 'commentList' 컬렉션의 'creatorId' 필드를 가져오기
  const getCreatorId = async (uid, id) => {
    const q = query(collection(db, 'commentList'));
    getDocs(q).then((querySnapshot) => {
      const firestorecommentlist = [];
      querySnapshot.forEach((doc) => {
        firestorecommentlist.push({
          id: doc.id,
          username: doc.data().username,
          creatorId: doc.data().creatorId,
        });
      });
      setCommentList(firestorecommentlist);
      // console.log(firestorecommentlist);
    });
  };
  useEffect(() => {
    getCreatorId();
  }, []);




  //! 여기서 민성 수정
  //! storage에 있는 모든 파일을 배열에 담아서 가져오기
  const getPhotoURL = async () => {
    const storageRef = ref(storage, 'images');
    const listRef = listAll(storageRef);
    listRef.then((res) => {
      res.items.forEach((itemRef) => {
        // console.log(itemRef);
        getDownloadURL(itemRef).then((url) => {
          // console.log(url);
          setPhotoURL(url);
        });
      });
    });
  };

  //! 다 내 사진이 뜸뜸
  // useEffect(() => {
  //   if (!currentUser) return;
  //   setPhotoURL(currentUser.photoURL);
  // }, [currentUser]);

  //! 나만 프로필 사진 뜨는 거
  useEffect(() => {
    // console.log(item);
    // console.log(currentUser);
    // console.log(item.creatorId);
    // console.log(currentUser.photoURL);
    // console.log(photoURL)

    if (currentUser.uid === item.creatorId) {
      setPhotoURL(currentUser.photoURL);
    } else {
      setPhotoURL('https://cdn.icon-icons.com/icons2/1378/PNG/512/avatardefault_92824.png');
      // https://item.kakaocdn.net/do/493188dee481260d5c89790036be0e66f604e7b0e6900f9ac53a43965300eb9a
    }
  }, [currentUser, item, photoURL]);

  //! 나만 프로필 사진 뜨는 거
  // useEffect(() => {
  //   console.log(item);
  //   console.log(currentUser);
  //   console.log(item.creatorId);
  //   console.log(currentUser.photoURL);
  //   console.log(photoURL)


  //   if (currentUser.uid === item.creatorId) {
  //     setPhotoURL(currentUser.photoURL);
  //   } else {
  //     setPhotoURL('');
  //   }
  // }, [currentUser, item, photoURL]);

  //! 수정중
  // useEffect(() => {
  //   setPhotoURL(photoURL);
  // }, [currentUser, photoURL, item]);

  //! 다 내 사진이 뜸
  // useEffect(() => {
  //   setPhotoURL(currentUser?.photoURL);
  // }, [currentUser, photoURL, item]);

  //! 다 내 사진이 뜸
  // useEffect(() => {
  //   if (currentUser.photoURL === item.creatorId) {
  //     setPhotoURL(currentUser?.photoURL);
  //   } else {
  //     setPhotoURL('https://item.kakaocdn.net/do/493188dee481260d5c89790036be0e66f604e7b0e6900f9ac53a43965300eb9a');
  //   }
  // }, [currentUser, item]);

  //! 아무도 안뜸뜸
  // useEffect(() => {
  //   if (currentUser.photoURL === item.creatorId) {
  //     setPhotoURL(currentUser.photoURL);
  //   } else {
  //     setPhotoURL('');
  //   }
  // }, [currentUser, item]);

  //! 여기까지 민성 수정

  const [profileUrl, setProfileUrl] = useState("");

  // const profileImgRef = ref(storage, '/profileImg/' + item.creatorId + '.png');
  // console.log(profileImgRef);

  // const imgUrl = getDownloadURL(
  //   ref(storage, profileImgRef))
  //   .then(url => setProfileUrl(url));

  // const splitDash = profileUrl?.split("/");
  // if ()
  // console.log(splitDash)
  // const splitDot = splitDash[7].split(".");
  // const userId = splitDot[0];
  // console.log(userId);


  return (
    <div style={{ marginTop: '50px' }}>
      <StCommentListContainer key={id}>
        {/* 작성자 정보 및 댓글 내용 */}
        <StCommentProfileImage src={photoURL} alt='' />
        <StCommentUserName>사용자 닉네임</StCommentUserName>
        <StCommentContentInput
          name='comment'
          readOnly={readOnly}
          defaultValue={comment}
          onChange={onChangeComment}
        />
        {/* 버튼 영역 - 수정 & 삭제 VS 완료 & 취소  */}
        {/* <span>{item.comment}</span> */}
        <StCommentContentSaveTime>{savetime}</StCommentContentSaveTime>

        {item.creatorId === currentUser.uid ? (
          modify ? (
            <>
              <StCommentContentsEditButton
                type='button'
                className='comment-edit-complete-btn'
                onClick={() => {
                  updateCompleteButtonHandler(id);
                }}
              >
                완료
              </StCommentContentsEditButton>
              <StCommentContentsDeleteButton
                onClick={() => {
                  editCancelButtonHandler(id);
                }}
              >
                취소
              </StCommentContentsDeleteButton>
            </>
          ) : (
            <>
              <StCommentContentsEditButton
                className='comment-edit-btn'
                onClick={() => {
                  updateCommentModify(id);
                }}
              >
                수정
              </StCommentContentsEditButton>
              <StCommentContentsDeleteButton
                onClick={() => {
                  deleteCommentButtonHandler(id);
                }}
              >
                삭제
              </StCommentContentsDeleteButton>
            </>
          )
        ) : null}
      </StCommentListContainer>
    </div>
  );
};

export default ShopGuideDetailsComment;

링크 // 참고자료

 

Link to Figma 

Link to Website

Link to Github

Link to demonstration video

 

 

 

 

 

What I felt

못 먹어도 고 였었고, 역시나 해치우진 못 했다. 다만, 겁도 없이 도전하는 건 지금이니까 가능한 것 아닐까? 어렵고 불가능해보이는 미션을 극복하고 해결해나간다는 드라마였고, 그저그런 도전이었다면 지금만큼이나 성장하지 못했을 것.

 

 

 


What Should We Keep 

시간적으로 어려운 것을 알고도 성장하기 위해 기꺼이 범의 굴에 발을 딛는 용기. 

 

Problem

초반에 파일 내부 규칙을 조금 더 세밀히 정했다면, 그대로 실현이 되었을까? 경험부족이라고 밖에 표현할 길이 없었던 무력함이 조금 있었다. 


Causation

 

경험 부족이 가장 크다. 프로젝트에 소요될 시간을 가늠할 수 없었고, 그 기능을 구현하는데 어느정도의 시간이 소요되는지,  추가적으로 소모되는 시간까지, 경험부족으로 귀결된다.

 

Try

 

미리 폴더 구조, 파일 구조를 조금 더 심사숙고해서 나누고, 공통적으로 사용할 것이라 여겨지는 코드를 미리 통일 했어도 좋았을 것

 

 

 

 

리액트 네이티브에선 일반적으로 사용되는 버튼 요소 두 가지가 있다. 

 

TouchableOpacity wrapper과 Button element이다.

 

하지만, 강의 중에서 실제론  TouchableOpacity wrapper 사용할 일이 더 많을 거란 이야기를 들었고, 살짝 TouchableOpacity와 Button의 점에 대해 궁금증이 일었다. 

 

THE Button Element

 

React Native Doc 에 따르면

 

Button element 는 모든 플랫폼에서 잘 렌더링되어야 하는 버튼이며, 최소 수준의 커스터마이징을 지원한다. 

 

버튼 요소를 단순하기 때문에, 표준 버튼이 필요할 경우 사용할 수 있고, 크기, 색상을 변경할 수 있으며, 어플리케이션에 추가하는 즉시 사용이 가능하게끔 설계 되어 있는 것으로 보인다. 하지만, Button 컴포넌트는 안드로이드와 ios에서 다르게 보이기 때문에 관리하는데에 어려움이 있다.

 

왼쪽) iOS Button, 오른쪽) Android Button

 

The TouchableOpacity Wrapper

 

마찬가지로, React Native Doc 에 따르면 

 

터치에 적절하게 반응하게끔 만드는 래퍼이다. 터치하면 그에 맞는 효과가 있다.

 

TouchableOpacity wrapper는 그 안에 담긴 모든 요소에 터치효과를 준다. 텍스트, 이미지 어느것이던 관계가 없으며, 기본적인 스타일이 지원되지 않아 처음부터 스타일링 해주어야 한다. 

 

왼쪽) iOS TouchableOpacity , 오른쪽) Android TouchableOpacity

이렇듯 CSS가 초기화 된 모습으로 나타나기에 style을 직접 주어야 한다. 

 

const TestScreen = () => {
  return (
    <View>
      <TouchableOpacity
        onPress={() => console.log("touchable opacity pressed")}
      >
        <Text>TouchableOpacityTest</Text>
      </TouchableOpacity>
    </View>
  )
}

 

const styles = StyleSheet.create({
  button: {
    alignItems: "center",
    backgroundColor: "blue",
    padding: 10
  },
  text: {
    color: "white"
  }
})

 

아래는 위의 STYLE을 적용 한 후이다. 

 

iOs, Android 모두 공평한 Touchable Opacity

 

 

 

 

import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Button, Text, View, TouchableOpacity, SafeAreaView, TextInput, ScrollView } from 'react-native';
import { Feather } from '@expo/vector-icons';
import { AntDesign } from '@expo/vector-icons';

export default function App() {
  const onPress = () => {
    console.log('button pressed');
  };


  const [text, onChangeText] = React.useState("Useless Text");

  return (
    // 버튼 세개 생성
    <SafeAreaView style={styles.container}>
      <View style={styles.buttonContainer}>
        <TouchableOpacity style={styles.button} onPress={onPress}><Text>JavaScript</Text></TouchableOpacity>
        <TouchableOpacity style={styles.button} onPress={onPress}><Text>React</Text></TouchableOpacity>
        <TouchableOpacity style={styles.button} onPress={onPress}><Text>Coding Test</Text></TouchableOpacity>
      </View>

      <View style={styles.lineDivider}></View>

      <TextInput
        style={styles.todoTextInput}
        onChangeText={onChangeText}
        value={text}
      />
      <View style={styles.lineDivider}></View>

      <ScrollView style={styles.listContainer}>

        <View style={styles.eachTodoCard}>
          <View style={styles.todoCard}><Text style={styles.todoText}>Todo List</Text></View>
          <View style={styles.fuctionBox}>
            <AntDesign name="checkcircleo" size={30} color="black" />
            <Feather name="edit" size={30} color="black" />
            <AntDesign name="delete" size={30} color="black" />
          </View>
        </View>
        <View style={styles.eachTodoCard}>
          <View style={styles.todoCard}><Text style={styles.todoText}>Todo List</Text></View>
          <View style={styles.fuctionBox}>
            <AntDesign name="checkcircleo" size={30} color="black" />
            <Feather name="edit" size={30} color="black" />
            <AntDesign name="delete" size={30} color="black" />
          </View>
        </View>
        <View style={styles.eachTodoCard}>
          <View style={styles.todoCard}><Text style={styles.todoText}>Todo List</Text></View>
          <View style={styles.fuctionBox}>
            <AntDesign name="checkcircleo" size={30} color="black" />
            <Feather name="edit" size={30} color="black" />
            <AntDesign name="delete" size={30} color="black" />
          </View>
        </View>

      </ScrollView>
    </SafeAreaView>


  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'space-evenly',
  },
  buttonContainer: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    width: '100%',
    backgroundColor: 'white',

  },
  listContainer: {
    flex: 1,
    width: '90%',
  },
  button: {
    backgroundColor: '#80ccff',
    width: 100,
    height: 40,

    alignItems: 'center',
    justifyContent: 'center',

    fontWeight: 'bold',
    fontSize: 30,

  },
  lineDivider: {
    width: '90%',
    height: 2,
    borderBottomColor: 'black',
    borderBottomWidth: 2,
    marginTop: 10,
    marginBottom: 10,
  },
  todoTextInput: {
    height: 40,
    width: '90%',
    paddingLeft: 5,
    borderColor: 'gray',
    borderWidth: 1,
  },
  todoCard: {
    marginTop: 5,
    paddingLeft: 5,
    height: 40,
    width: '65%',
    borderWidth: 1,
    borderColor: 'gray',
    justifyContent: 'center',

  },
  todoText: {
    fontSize: 16,
  },
  fuctionBox: {
    display: 'flex',
    flexDirection: 'row',
    justifyContent: 'space-evenly',
    alignContent: 'center',
    marginTop: 5,
    paddingTop: 2,
    width: '35%',
    height: 40,
    borderWidth: 1,
    borderColor: 'gray',

  },
  eachTodoCard: {
    flexDirection: 'row',
  }



});
'Redux Toolkit은 Redux 로직을 작성하기 위해 저희가 공식적으로 추천하는 방법입니다. RTK는 Redux 앱을 만들기에 필수적으로 여기는 패키지와 함수들을 포함합니다. 대부분의 Redux 작업을 단순화하고, 흔한 실수를 방지하며, Redux 앱을 만들기 쉽게 해주는 모범 사례를 통해 만들어졌습니다.'
리덕스 공식문서

 

 

Redux Toolkit Useage

Library Doownload

Redux Toolkit 설치 방법

 

에디터나 커맨드 창에서 아래를 입력합니다.

// Install Redux Library
$ npm install @reduxjs/toolkit react-redux

 

 

 

React - Redux Toolkit 의 API 를 참고하면, 크게 3가지로 나누어져 있다. 

 

 

1. Store Setup

2. Reducers and Actions

3. Other

 

API에 따르면 Store Setup 단계는 

 

1. Store Setup 

  • configureStore
    • 표준 Redux createStore 기능의 익숙한 추상화로, 보다 나은 개발 환경을 위해 스토어 설정에 좋은 기본값을 추가합니다.
  • getDefaultMiddleware
    • 기본 미들웨어 목록을 포함하는 배열을 반환합니다.
  • Immutability Middleware
    • Redux Toolkit과 함께 사용하기 위해 사용자 지정된 Redux 불변 상태 미들웨어의 포트입니다. 발견된 모든 돌연변이는 오류로 간주됩니다.
      이 미들웨어는 configureStore 및 getDefaultMiddleware에 의해 스토어에 기본적으로 추가됩니다.
      지원되는 옵션 중 하나를 getDefaultMiddleware의 변경 불가능한 Check 값으로 전달하여 이 미들웨어의 동작을 사용자 정의할 수 있습니다.
  • Serializability Middleware
    • 직렬화할 수 없는 값이 상태 또는 디스패치된 작업에 포함되었는지 여부를 탐지하는 사용자 정의 미들웨어로, 중복-불변-상태-불변을 본떠서 모델링합니다. 탐지된 직렬화 불가능한 값은 콘솔에 기록됩니다.
      이 미들웨어는 configureStore 및 getDefaultMiddleware에 의해 스토어에 기본적으로 추가됩니다.
      지원되는 옵션 중 하나를 getDefaultMiddleware의 serializableCheck 값으로 전달하여 이 미들웨어의 동작을 사용자 정의할 수 있습니다.
  • createListenerMiddleware
    • 추가 로직을 포함하는 "효과" 콜백을 포함하는 "수신기" 항목을 정의하고, 발송된 작업 또는 상태 변경을 기반으로 콜백을 실행할 시기를 지정하는 방법을 사용할 수 있는 Redux 미들웨어입니다.
      이것은 사가나 관찰 가능한 것과 같은 더 널리 사용되는 레덱스 비동기 미들웨어의 가벼운 대안이 되기 위한 것이다. 복잡성 및 개념 수준에서 thunk와 유사하지만 일부 일반적인 사가 사용 패턴을 복제하는 데 사용할 수 있다.
      개념적으로, 구성 요소 속성/상태 업데이트 대신 Redux 스토어 업데이트에 응답하여 논리를 실행한다는 점을 제외하고는 React의 useEffect 후크와 유사하다고 생각할 수 있습니다.
      수신기 효과 콜백은 think와 유사하게 디스패치 및 getState에 액세스할 수 있습니다. 수신기는 또한 take, condition, pause, fork, unsubscribe와 같은 비동기 워크플로우 기능을 수신하므로 더 복잡한 비동기 논리를 작성할 수 있습니다.
      수신기는 설정 중에 listenerMiddleware.startListening()을 호출하여 정적으로 정의하거나, 특별한 디스패치(addListener() 및 디스패치(removeListener() 액션을 사용하여 런타임에 동적으로 추가 및 제거할 수 있습니다.
  • autoBatchEnhancer
    • 하나 이상의 "낮은 우선순위" 발송 작업을 한 줄로 찾고, 지연 시 가입자 알림을 실행하기 위해 콜백을 대기하는 Redux 스토어 향상 프로그램입니다. 그런 다음 대기 중인 콜백이 실행될 때 또는 다음 "일반 우선순위" 작업이 발송될 때 먼저 가입자에게 알립니다.

 

우선 이중에서 Store Setup부터 시작하겠다.

 
 

Redux Toolkit | Redux Toolkit

The official, opinionated, batteries-included toolset for efficient Redux development

redux-toolkit.js.org

redux toolkit 웹사이트 

 

 

Redux Toolkit 설치

 

#NPM

npm install @reduxjs/toolkit

#Yarn

yarn add @reduxjs/toolkit

위의 명령어로 우선 Redux Toolit 을 설치하면 리덕스가 함께 설치 된다. 

 

 

 

 

createStore > configureStore

 

<기존 createStore 문법 사용 예시>

import { createStore, applyMiddleware } from "redux";
import { composeWithDevTools } from "redux-devtools-extenstion";
import thunk from "redux-thunk";
import rootReducer from "./reducers";

let store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(thunk));
);

export default store;

 

<변경된 configure 문법 사용 예시>

 

import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'

const store = configureStore({ reducer: rootReducer });

export default store;

 

<변경 시사점>

 

리덕스 버전 업데이트로 인해 더 이상의 createStore 기능은 지원되지 않는다. 다만, 2022년 12월 19일 기준으로는 유예기간에 따라, IDE 내에서 줄선이 그러짐을 확인할 수 있다. 

 

업데이트를 통해 createStore을 사용할 때의 버젼에서보다 편의성이 더욱 증대되었다고 한다. 

 

STATE

 

컴포넌트 내부적을 사용하는 데이터의 집합. state는 어떤 event나 API등을 통해 setState 메서드를 사용하여 바뀔 수 있는 것을 배웠다. 그리고 앱은 state를 읽어와서 화면에 그에 맞는 UI를 출력하게 된다. 

 

STORE

 

Store는 state의 관리를 하는 전용 장소로,

state들이 store에 객체형식으로 저장된다. 규모가 클 경우에는 state를 카테고리별로 분류하는 경우가 일반적이라고 한다.

  • createStore(reducer)로 Redux store를 생성할 수 있다.
  • getState()를 통해 현재 state를 가져올 수 있다.
  • dispatch(action)을 사용하여, store의 reducer에 action을 전달한다. redux에서 상태 변경을 일으킬 수 있는 유일한 방법이다.

 

REDUCER

 

리듀서는 두가지의 파라미터를 받아 변화를 일으키는 함수. 간단히만 말하자면 Store에 대해 뭔가를 하고싶거나 store의 state를 업데이트하고 싶을 때 발생하는 이벤트 드리븐같은 것.  Action은 Reducer로 전달된다.Action을 전달받는 Reducer는 각 Action이 Store를 어떻게 업데이트할지를 기술하는 순수함수. 

 

DISPATCH

 

디스패치는 스토어의 내장함수 중 하나. state 변경을 해주는 액션을 스토어까지 가져와주는 스토어 관리자(Dispatcher)가 스토어의 상태 변경을 관리

 

SUBSCRIBE

 

구독 또한 스토어의 내장함수 중 하나. subscribe 함수는, 함수 형태의 값을 파라미터로 받아 subscribe 함수에 특정 함수를 전달해주고 액션이 디스패치 되었을 때 마다 전달해준 함수가 호출.

 

 

 

ACTION

 

Action은 상태 변경을 일으키는 이벤트에 대한 정적인 정보이다. Reducer가 Action과 이전 state을 참고해서 새로운 state를 만들기 때문에 Action은 reducer가 구분 할 수 있도록 액션의 이름(타입) 과 데이터를 가진 객체형식이다. 

 

 

 

+ Recent posts