[error shooting] 상태 끌어 올리기

이로운·2023년 3월 23일
0

react

목록 보기
2/2

상태 끌어올리기

react에는 state를 사용하여 상태를 관리한다
하지만 리덕스나 리코일이 아닌 일반적인 상태관리 상황에서는 부모에서 자식으로만(단방향으로만) 관리를 한다
하지만 이런 상황이 생긴다

에러상황

//자식 컴포넌트
import { useState } from 'react'
import isEmptyValue from '../utils/isEmptyValue';

function Tag(){
  const [inputHashTag, setInputHashTag] = useState('');
  const [hashTags, setHashTags] = useState([]);

  const addHashTag = (e) => {
    const allowedCommand = ['Comma', 'Enter', 'Space', 'NumpadEnter'];
    if (!allowedCommand.includes(e.code)) return;
    if ([...hashTags].length >= 3) return;
  
    if (isEmptyValue(e.target.value.trim())) {
      return setInputHashTag('');
    }
  
    let newHashTag = e.target.value.trim();
    // const regExp = /[\{\}\[\]\/?.;:|\)*~`!^\-_+<>@\#$%&\\\=\(\'\"]/g;
    // if (regExp.test(newHashTag)) {
    //   newHashTag = newHashTag.replace(regExp, '');
    // }
    if (newHashTag.includes(',')) {
      newHashTag = newHashTag.split(',').join('');
    }
  
    if (isEmptyValue(newHashTag)) return;
  
    setHashTags((prevHashTags) => {
      return [...new Set([...prevHashTags, newHashTag])];
    });
  
    setInputHashTag('');
  };
  
  const keyDownHandler = (e) => {
    if (e.code !== 'Enter' && e.code !== 'NumpadEnter') return;
    e.preventDefault();
  
    const regExp = /^[a-z|A-Z|가-힣|ㄱ-ㅎ|ㅏ-ㅣ|0-9| \t|]+$/g;
    if (!regExp.test(e.target.value)) {
      setInputHashTag('');
    }
  };
  
  const changeHashTagInput = (e) => {
    setInputHashTag(e.target.value);
  };

  const deleteTagItem = e => {
    const deleteTagItem = e.target.parentElement.firstChild.innerText
    const filteredTagList = hashTags.filter(hashTags => hashTags !== deleteTagItem)
    setHashTags(filteredTagList)
  }

    
  return(
    <div className='bg-[#EAEAEA] w-[334px] h-[57px] rounded-[15px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)] flex items-center flex-wrap min-h-[50px] px-[10px] focus-within:bg-tomato'>
      {hashTags.map((hashTag, index) => {
          return (
            <div key={index} className='flex justify-between items-center m-[5px] p-[5px] bg-[tomato] rounded-[5px] text-white text-[13px] min-w-[50px]'>
              <span className='pt-[3px] pl-[2px]'>{hashTag}</span>
              <button onClick={deleteTagItem} className='flex justify-center items-center w-[15px] h-[15px] ml-[5px] mt-[3px]'>X</button>
            </div>
          )
        })}
        <input type='text' placeholder='태그를 입력해주세요' onChange={changeHashTagInput} value={inputHashTag} onKeyUp={addHashTag} onKeyDown={keyDownHandler} className='inline-flex bg-transparent border-none outline-none cursor-text' />
    </div> 
  )
      }


export default Tag
//부모 컴포넌트
import { dbService } from 'fbase';
import { v4 as uuid4 } from "uuid";
import { useRef, useState } from 'react';
import { storageService } from '../fbase';
import { addDoc, collection } from 'firebase/firestore';
import { getDownloadURL, ref, uploadBytes } from "@firebase/storage";
import ChallengeSubmitButton from './SubmitButton/ChallengeSubmitButton';
import FileInput from './TextInput/FileInput';
import Tag from './Tag';

function ImgInput(){
  const [challenge, setChallenge] = useState("");
  const [attachment, setAttatchment] = useState("");
  const [hashtag, setHashtag] = useState([]);
  const fileInput = useRef();
  const onSubmit = async (e) => {
    e.preventDefault();
    let attachmentUrl = '';
    if(attachment !== ""){
      const attachmentRef = ref(storageService, `${uuid4()}`);
      const response = await uploadBytes(attachmentRef, attachment, "data_url");
      attachmentUrl = await getDownloadURL(response.ref)
    } 
    const challengObj = {
      challenge,
      createdAt: Date.now(),
      attachmentUrl,
      hashtag : hashtag
    }
    await addDoc(collection(dbService, "challenges"),challengObj);
    setChallenge('');
    setAttatchment("");
    setHashtag([]);
  }
  const onChange = (e) => {
    const {
      target: { value },
    } = e
    setChallenge(value)
  };
  const onFileChange = (e) => {
    const {
      target : { files },
    } = e;
    const theFile = files[0]
    const reader = new FileReader();
    reader.onloadend = (finishedEvent) => {
      const{
        currentTarget: {result}
      } = finishedEvent
      setAttatchment(result);
    };
    reader.readAsDataURL(theFile);
  };
  const onTagChange = (e) => {
    const {
      target: { value },
    } = e
    setHashtag(value);
  } 
  const onClearAttachment = () => {
    setAttatchment(null);
    fileInput.current.value = null;
  };

  return (
    <>
      <form name='recruitment' onSubmit={onSubmit}>
        <div className='text-h3 text-gray mt-[26px]'>사진등록</div>
        <FileInput onChange={onFileChange} ref={fileInput}>
          {attachment && <img className='-indent-[9999px] block m-auto w-full h-full' src={attachment} alt="이미지"/>}
        </FileInput>
        <div className='mt-[47px] mb-[10px] text-gray'>제목</div>
        <textarea className='bg-[#EAEAEA] w-[334px] h-[57px] rounded-[15px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)] cursor-text resize-none indent-3.5 pt-[18px]' value={challenge} onChange={onChange} placeholder='내용을 입력해 주세요' maxLength='20' ref={fileInput}></textarea>
        <div className='mt-[47px] mb-[10px] text-gray'>태그</div>
        <Tag onChange={onTagChange} ref={fileInput} />
        <ChallengeSubmitButton type='submit' onClick={onClearAttachment}>완료</ChallengeSubmitButton>
      </form>
    </>
  )
}

export default ImgInput

해쉬태그를 firebase로 보내야 하는 상황,
보내지긴 하지만 배열로된 해쉬태그들을 못 보내고 있었다.

오류 파악

생각을 해보니 자식 컴포넌트에 hashTags라는 배열이 있어서 그걸 부모까지 못가지고 오는 상황이었다

해결 방안

상태 끌어올리기를 통해서 부모에서 상태를 관리하도록 해줘야겠다는 생각이 들었다
일반적인 상황에서는 부모 -> 자식의 방향으로 props를 줘야하기 때문에 useState를 부모로 가지고 와서
props를 주고 자식은 ...restProps로 props들을 받아 뿌려줘야한다

해결

//자식
import isEmptyValue from '../utils/isEmptyValue';

function Tag({...restProps}){
  const { inputHashTag, handleInputHashTag, hashTags, handleHashTags} = restProps;
  const addHashTag = (e) => {
    const allowedCommand = ['Comma', 'Enter', 'Space', 'NumpadEnter'];
    if (!allowedCommand.includes(e.code)) return;
    if ([...hashTags].length >= 3) return;
  
    if (isEmptyValue(e.target.value.trim())) {
      return handleInputHashTag('');
    }
  
    let newHashTag = e.target.value.trim();
    // const regExp = /[\{\}\[\]\/?.;:|\)*~`!^\-_+<>@\#$%&\\\=\(\'\"]/g;
    // if (regExp.test(newHashTag)) {
    //   newHashTag = newHashTag.replace(regExp, '');
    // }
    if (newHashTag.includes(',')) {
      newHashTag = newHashTag.split(',').join('');
    }
  
    if (isEmptyValue(newHashTag)) return;
  
    handleHashTags((prevHashTags) => {
      return [...new Set([...prevHashTags, newHashTag])];
    });
  
    handleInputHashTag('');
  };
  
  const keyDownHandler = (e) => {
    if (e.code !== 'Enter' && e.code !== 'NumpadEnter') return;
    e.preventDefault();
  
    const regExp = /^[a-z|A-Z|가-힣|ㄱ-ㅎ|ㅏ-ㅣ|0-9| \t|]+$/g;
    if (!regExp.test(e.target.value)) {
      handleInputHashTag('');
    }
  };
  
  const changeHashTagInput = (e) => {
    handleInputHashTag(e.target.value);
  };

  const deleteTagItem = e => {
    const deleteTagItem = e.target.parentElement.firstChild.innerText
    const filteredTagList = hashTags.filter(hashTags => hashTags !== deleteTagItem)
    handleHashTags(filteredTagList)
  }

    
  return(
    <div className='bg-[#EAEAEA] w-[334px] h-[57px] rounded-[15px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)] flex items-center flex-wrap min-h-[50px] px-[10px] focus-within:bg-tomato'>
      {hashTags.map((hashTag, index) => {
          return (
            <div key={index} className='flex justify-between items-center m-[5px] p-[5px] bg-[tomato] rounded-[5px] text-white text-[13px] min-w-[50px]'>
              <span className='pt-[3px] pl-[2px]'>{hashTag}</span>
              <button onClick={deleteTagItem} className='flex justify-center items-center w-[15px] h-[15px] ml-[5px] mt-[3px]'>X</button>
            </div>
          )
        })}
        <input type='text' placeholder='태그를 입력해주세요' onChange={changeHashTagInput} value={inputHashTag} onKeyUp={addHashTag} onKeyDown={keyDownHandler} className='inline-flex bg-transparent border-none outline-none cursor-text' />
    </div> 
  )
      }


export default Tag
// 부모
import { dbService } from 'fbase';
import { v4 as uuid4 } from "uuid";
import { useRef, useState } from 'react';
import { storageService } from '../fbase';
import { addDoc, collection } from 'firebase/firestore';
import { getDownloadURL, ref, uploadBytes } from "@firebase/storage";
import ChallengeSubmitButton from './SubmitButton/ChallengeSubmitButton';
import FileInput from './TextInput/FileInput';
import Tag from './Tag';

function ImgInput(){
  const [challenge, setChallenge] = useState("");
  const [attachment, setAttatchment] = useState("");
  const [inputHashTag, setInputHashTag] = useState('');
  const [hashTags, setHashTags] = useState([]);
  const fileInput = useRef();
  const onSubmit = async (e) => {
    e.preventDefault();
    let attachmentUrl = '';
    if(attachment !== ""){
      const attachmentRef = ref(storageService, `${uuid4()}`);
      const response = await uploadBytes(attachmentRef, attachment, "data_url");
      attachmentUrl = await getDownloadURL(response.ref)
    } 
    const challengObj = {
      challenge,
      createdAt: Date.now(),
      attachmentUrl,
      hashTags
    }
    await addDoc(collection(dbService, "challenges"),challengObj);
    setChallenge('');
    setAttatchment("");
    setHashTags([]);
  }
  const onChange = (e) => {
    const {
      target: { value },
    } = e
    setChallenge(value)
  };
  const onFileChange = (e) => {
    const {
      target : { files },
    } = e;
    const theFile = files[0]
    const reader = new FileReader();
    reader.onloadend = (finishedEvent) => {
      const{
        currentTarget: {result}
      } = finishedEvent
      setAttatchment(result);
    };
    reader.readAsDataURL(theFile);
  };
  const handleInputHashTag = (val) => {
    setInputHashTag(val);
  }
  const handleHashTags = (val) => {
    setHashTags(val);
  }

  return (
    <>
      <form name='recruitment' onSubmit={onSubmit}>
        <div className='text-h3 text-gray mt-[26px]'>사진등록</div>
        <FileInput onChange={onFileChange} ref={fileInput}>
          {attachment && <img className='-indent-[9999px] block m-auto w-full h-full' src={attachment} alt="이미지"/>}
        </FileInput>
        <div className='mt-[47px] mb-[10px] text-gray'>제목</div>
        <textarea className='bg-[#EAEAEA] w-[334px] h-[57px] rounded-[15px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)] cursor-text resize-none indent-3.5 pt-[18px]' value={challenge} onChange={onChange} placeholder='내용을 입력해 주세요' maxLength='20' ref={fileInput}></textarea>
        <div className='mt-[47px] mb-[10px] text-gray'>태그</div>
        <Tag ref={fileInput} inputHashTag={inputHashTag} handleInputHashTag={handleInputHashTag} hashTags={hashTags} handleHashTags={handleHashTags}/>
        <ChallengeSubmitButton type='submit'>완료</ChallengeSubmitButton>
      </form>
    </>
  )
}

export default ImgInput


잘 되는 모습이다.

profile
이름 값 하는 개발자가 꿈인 사람

0개의 댓글