React 부트캠프 TIL 22

정다롱·2024년 9월 8일

내일배움캠프 TIL

목록 보기
21/39

🖥️ 작성 페이지 컴포넌트 그대로 수정 페이지 만들기


🗂️ 작성 페이지 컴포넌트 구조

const WritePage = () => {
  return (
    <WriteProvider>
      <WriteContainer>
        <MainContainer>
          <Title>나만의 레시피 등록하기</Title>
          <Content>
            <RecipeInfo />
          </Content>
          <Content>
            <Ingredients />
            <RecipeCont />
          </Content>
          <Content>
            <SaveBox />
          </Content>
        </MainContainer>
      </WriteContainer>
    </WriteProvider>
  );
};

🗂️ 수정 페이지 컴포넌트 구조

const EditPage = () => {
  return (
    <WriteProvider>
      <WriteContainer>
        <MainContainer>
          <Title>나만의 레시피 수정하기</Title>
          <Content>
            <RecipeInfo />
          </Content>
          <Content>
            <Ingredients />
            <RecipeCont />
          </Content>
          <Content>
            <SaveBox />
          </Content>
        </MainContainer>
      </WriteContainer>
    </WriteProvider>
  );
};

똑 . 같 . 다

하지만 작성 페이지에서는 저장버튼을 누르면 새로 데이터가 올라가야하고 수정 페이지에서는 애초에 인풋에 사용자가 입력했던 데이터가 채워져 있어야하며 저장버튼을 누르면 기존에 있던 데이터가 수정되는 형태로 동작해야했다.

그. 래. 서!

✅ Write Context 내부에서 로직 분리하기 - 어떻게?

  // edit인가요?
  const pathArray = window.location.pathname.split("/");
  const path = pathArray[pathArray.length - 2];

  // 게시글 아이디 따와
  const params = useParams();
  const editId = Number(params.id);

상세 페이지 연결이 /detail/RECIPE_ID 로 되고 있고, 수정 페이지 주소는 /edit/RECIPE_ID 로 받고 있기 때문에 URL에서 ID값을 따올 수 있다.

그래서 한 컨텍스트 안에서 수정 페이지일때, 작성 페이지일때 다르게 동작하도록 할 수 있다!


✅ 수정 페이지 일때 기존 데이터 불러오기

 // edit 일때는 밸류 채워주기
  useEffect(() => {
    if (path === "edit") {
      const getData = async () => {
        const { data: info } = await supabase.from("recipe_info").select("*").eq("RECIPE_ID", editId);
        setRecipeInfo(...info);
        setImageSrc(info[0].RECIPE_IMG);

        const { data: ingInfo } = await supabase
          .from("recipe_ingredient")
          .select("ING_NAME,ING_VOL")
          .eq("RECIPE_ID", editId);
        setIngredientGroups(ingInfo);

        const { data: cont } = await supabase
          .from("recipe_flow")
          .select("RECIPE_STEP, RECIPE_CONT")
          .eq("RECIPE_ID", editId);
        setRecipeContGroups(cont);
      };
      getData();
    }
  }, [path, editId]);

최초 렌더링시에 채워주면 되기 때문에 useEffect 를 사용했다. 의존성 배열에는 path, editId를 넣어서 혹시 링크가 변경되거나 할 경우를 대비했는데... 주소로 이동하는 걸 막아놨어야 했다는 걸 지금 깨달았다 헤헤

 if (path === "edit") // 만약, 수정 페이지라면
	// recipe_info 테이블에서 RECIPE_ID가 일치하는 row 다 불러오기
    const { data: info } = await supabase.from("recipe_info").select("*").eq("RECIPE_ID", editId);
    setRecipeInfo(...info); // 가져온 데이터 state에 저장
	setImageSrc(info[0].RECIPE_IMG); // 미리보기 이미지도 띄우기 
	// 재료 테이블에서도 데이터 가져와서 재료 state에 저장 - 레시피 순서도 똑같이!
	const { data: ingInfo } = await supabase
          .from("recipe_ingredient")
          .select("ING_NAME,ING_VOL")
          .eq("RECIPE_ID", editId);
        setIngredientGroups(ingInfo);
	getData(); // 잊지 말고 useEffect 내부에서 함수 호출하여 실행시키기

근데 여기서 보면 전체 데이터를 안 가져오고 select("ING_NAME,ING_VOL") 로 특정 컬럼만 가져오는 걸 볼 수 있는데 여기에 큰 사정이 있었다. 아래에 트러블 슈팅으로 써둠...


✅ 불러온 데이터 value에 넣어주기

// input 컴포넌트

const Input = ({ place, onChange, type, index, table }) => {
  const { recipeInfo, ingredientGroups, recipeContGroups } = useContext(WriteContext);

  let value = "";
  if (table === "info") {
    value = recipeInfo?.[type]; // recipeInfo에서 type에 해당하는 값
  } else if (table === "ing") {
    value = ingredientGroups[index]?.[type]; // ingredientGroups 배열에서 index에 해당하는 값
  } else if (table === "flow") {
    value = recipeContGroups[index]?.[type]; // recipeContGroups 배열에서 index에 해당하는 값
  }

  return <InputStyled value={value} placeholder={place} onChange={(e) => onChange(e.target.value, type, index)} />;
};
let value = ""; // 초기 value 빈 문자열로 설정

데이터가 저장되는 테이블을 알려주는 table,
어떤 종류의 값을 입력받고 있는지 알려주는 type
두개를 props로 받고 있기 때문에 정보, 재료, 순서 인풋을 올바르게 채워줄 수 있다.

정보의 경우 객체 형태로 들어오기 때문에 type 만 지정해주었고
재료, 순서의 경우 배열 안에 객체가 있는 형태로 들어오기 때문에 index까지 지정해 사용자가 입력했던 위치에 입력했던 값이 들어갈 수 있도록 설정했다.


✅ 그럼 저장은 어떻게?

 const saveRecipe = async () => {

똑같은 saveRecipe 함수를 사용한다. 유효성 검사는 똑같이 진행되어야 하니까...

if (path === "edit") {
      let uploadedImageUrl = recipeInfo.RECIPE_IMG; // 기존 이미지 URL로 초기화

      // 이미지가 변경된 경우에만 새 이미지를 업로드
      if (fileInputRef.current.files.length > 0) {
        const file = fileInputRef.current.files[0];
        const fileName = `${Date.now()}_${file.name}`;

        const { data, error } = await supabase.storage
          .from("foodimg") // 버킷 이름 변경
          .upload(`images/${fileName}`, file);

        if (error) {
          console.error("이미지 업로드 중 오류 발생:", error.message);
          return; // 이미지 업로드 실패 시, 더 이상 진행하지 않도록 종료
        }

        uploadedImageUrl = supabase.storage.from("foodimg").getPublicUrl(`images/${fileName}`).data.publicUrl;
      }

      const updateRecipeCont = recipeContGroups.map((cont, index) => ({
        ...cont,
        RECIPE_STEP: index + 1,
        RECIPE_ID: editId
      }));

      const updateIng = ingredientGroups.map((ing) => ({
        ...ing,
        RECIPE_ID: editId
      }));

      await supabase
        .from("recipe_info")
        .upsert([{ ...recipeInfo, RECIPE_IMG: uploadedImageUrl }])
        .eq("RECIPE_ID", editId);

      await supabase.from("recipe_ingredient").delete().eq("RECIPE_ID", editId);
      await supabase.from("recipe_ingredient").insert(updateIng);
      await supabase.from("recipe_flow").delete().eq("RECIPE_ID", editId);
      await supabase.from("recipe_flow").insert(updateRecipeCont);

      navigate(`/detail/${editId}`);

      return; // 일찍 종료
}

우와 길다
앞에서 처럼 조건문을 걸어서 작성 페이지일때는 조건문 안의 로직만 수행하고 함수가 종료된다.
이미지 변경 부분은 TIL 따로 작성할 예정이기 때문에 여기서는 넘어가겠다.

	// 레시피 순서의 STEP을 입력할 때 지정하니까 추가, 삭제시 꼬이는 현상이 있어서 아예 초기값에서
    // 지우고 DB에 올리기 전에 지정해주기로 했다.
	const updateRecipeCont = recipeContGroups.map((cont, index) => ({
        ...cont,
        RECIPE_STEP: index + 1,
        RECIPE_ID: editId
      }));

	// 데이터를 불러 올 때 ID 컬럼은 제외했기 때문에 등록 전에 추가해주는 부분
    const updateIng = ingredientGroups.map((ing) => ({
        ...ing,
        RECIPE_ID: editId
      }));
      await supabase
        .from("recipe_info")
        .upsert([{ ...recipeInfo, RECIPE_IMG: uploadedImageUrl }])
        .eq("RECIPE_ID", editId);
  • upsert : 같은 ID값을 가진 ROW가 있으면 입력한 값으로 수정해주고, 같은 ID값을 가진 ROW가 없으면 추가해준다. (update 역할. update도 따로 있긴 함... 그런데 안 써가지고 뭐가 다른지는... 모르겠음...)
	// 이 레시피 재료, 순서 싹 다 지우고 다시 올릴게요...
      await supabase.from("recipe_ingredient").delete().eq("RECIPE_ID", editId);
      await supabase.from("recipe_ingredient").insert(updateIng);
      await supabase.from("recipe_flow").delete().eq("RECIPE_ID", editId);
      await supabase.from("recipe_flow").insert(updateRecipeCont);

왜??

그러니까 말입니다.


💥트러블 슈팅

나를 다섯시간 동안 튜터님이랑 좋은 시간을 보내게한 주범.
바로 upsert, update 이 두녀석이다.

update

  • Perform an UPDATE on the table or view.
  • update() should always be combined with Filters to target the item(s) you wish to update.

supabase 공식 docs에 나와있는 내용이다.
테이블에서 업데이트를 수행하며, 항상 선택자와 함께 사용해야한다는 내용.

그리고 예시 코드

const { error } = await supabase
  .from('countries')
  .update({ name: 'Australia' })
  .eq('id', 1)

....
그런데 내가 몰랐던 사실....

supabase update는... 한 개의 row 데이터만 수정한다...
그리고 나는 계속 update 안에 insert처럼 배열을 넣어 여러개의 객체를 한 번에 수정하고자 시도했고, 당연하게도 오류가 발생했다. 제대로 검색도 하지 않고 오로지 내가 원래 알고 있는 update의 의미에만 집중했으니 해결이 될리가 없었고, 결국 해결책을 찾지 못한 채 upsert로 노선을 변경했다.

upsert

  • Perform an UPSERT on the table or view. Depending on the column(s) passed to onConflict, .upsert() allows you to perform the equivalent of .insert() if a row with the corresponding onConflict columns doesn't exist, or if it does exist, perform an alternative action depending on ignoreDuplicates.
  • Primary keys must be included in values to use upsert.

supabase 공식 docs에서 가져왔다.
upsert는 update, insert를 합친 것 처럼 동작한다. 특정 고유값이 중복이라면 update를, 중복이 아니라면 insert를 수행하는?

아까 위에서 주석으로 남겼던

	const { data: ingInfo } = await supabase
          .from("recipe_ingredient")
          .select("ING_NAME,ING_VOL")

이 부분이 원래는 select("*") 이었다. 모든 데이터를 가져와서 내용만 수정하고 다시 업데이트 할 생각이었고, 그 과젱에서 새로 추가된 테이블이 있다면 insert가 되도록 처리하고 싶었다.

하지만 아무리 해봐도... 동작하지 않았다... 그리고 나는 계속 시도했다

  • Primary keys must be included in values to use upsert.

이 부분이 당연히!!! update 부분에만 적용되는 건 줄 알았기 때문이다...

오류는 계속해서 ING_ID가 null이라고 외쳐댔고, 나는 너무 답답했다!!! 아니 당연히 ING_ID가 없겠지!!! 그건 니들이 새롭게 만드는 고유값이니까!! 라고 생각하며 어디가 잘못됐나 여기저기 수정해봤지만 되지 않았다.

무엇이 문제였을까?

  1. update - 하나의 row 데이터만 수정이 가능하다. (필터를 통해 수정할 row를 지정해야함)
  2. upsert - 모든 데이터에 ID를 입력해줘야함. (그 아이디가 중복이면 update, 없으면 insert)

너무 허무했다.. 그러니까 애초에 내가 기대했던 '있으면 수정하고 없으면 추가해줘' 는 수파베이스에서 할 수 없었다. 아 물론 방법이 있을 수도 있다. 사용방법을 잘 모르기도 하고, 검색을 잘못해서 내가 방법을 못찾았을 가능성이 크다. SQL문으로 어떻게 되는 것 같긴 하던데 거기까진 별로 손대고 싶지 않아서 그냥 필터를 통해 ID가 있으면 upsert, 없으면 insert하는 로직으로 수정했다.

하지만 이 로직에도 문제가 있었다! 삭제된 재료나 순서가 반영되지 않았다. 사실 생각해보면 당연한건데, 나는 그 당연한걸 수파베이스가 해주길 바랐기 때문에 좀 답답했던 것 같다. insert, upsert가 정상적으로 작동되는 걸 확인하고 새로고침하니 재료와 순서가 두배가 되어있었다. 테스트한다고 삭제하고 추가했던 데이터들이 추가는 됐는데 삭제가 안 됐기 때문에. 당연하다! delete를 안했으니까!

그래서 최종적으로 모든 데이터를 삭제하고 새로 추가하는 지금의 로직이 완성되었다.

onConflict ?

지금 검색하다 봤는데, upsert에는 onConflict라는 옵션이 있는 모양이다. 이게 insert를 할지, upsert를 할지 지정하는 특정값을 정하는 옵션인 것 같은데 이걸 사용하면 됐을까? 싶기도 해서 나중에 한번 따로 찾아봐야겠다!! 검색하다 찾은 질문?

0개의 댓글