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
잘 되는 모습이다.