블로그
전체 11#태그
- 워밍업클럽
2024. 11. 03.
2
[인프런 워밍업 스터디 클럽 2기 FE] 오프라인 수료식 후기
오프라인 수료식 후기 퇴근하고 판교까지 늦지 않게 갈 수 있을까 걱정했는데 다행히 여유 있게 도착해서 이름표를 받았다.안으로 들어가니 이미 많은 분들이 일찍 도착해 계셨다. 코치님이랑 다른 러너분들이랑 자기소개도 하고 피자 먹으면서 자유 네트워킹 시간을 가졌다. 발자국을 남길 때 봤던 분들을 직접 만나게 되어서 좋았다. 다들 좋으신 분들이라 오프라인 수료식에 갈지 말지 고민했던 게 무색하게 재밌던 시간이 되어서 오길 잘했다는 생각이 들었다. 잠깐의 쉬는 시간이 지나고 시작된 Q&A 시간도 좋았다. 코치님께서 하나하나 해주시는 답변들이 정말 큰 도움이 되었다. 메모를 조금 하고 싶었는데 필기도구도 안 챙겼고.. 핸드폰에 쓰면 딴짓하는 사람처럼 보일까 봐 최대한 기억만 했다.그리고 하고 싶었던 질문이 있어서 할까 말까 고민하고 있었는데 마침 다른 분이 물어봐 주셔서 내 궁금증도 같이 해소가 되었다. 다른 분들은 어떤 고민을 하고 어떤 생각을 하시는지 들을 수 있는 점도 좋았다.궁금했던 부분도 해결하고 모르는 것들도 많이 알게 되어서 유익한 시간이었다. 자유 네트워킹, Q&A 시간이 너무 빠르게 지나간 것 같아서 아쉽다는 생각이 들었다. 다른 분들이랑 얘기를 더 나누고 싶었다. 이번에 워밍업클럽 신청하길 잘한 것 같다. 3주동안 열심히 했던 만큼 성장한 부분이 있다고 생각한다. 귀여운 인프런 굿즈가 생겼다 ✌
워밍업클럽
2024. 10. 15.
0
[인프런 워밍업 스터디 클럽 2기 FE] 3주차 과제 - 쇼핑몰 앱 만들기
리덕스를 이용한 쇼핑몰 앱 만들기github : 12-shopping-mall 기능목록json 데이터로 상품 리스트 출력카테고리 별 상품 출력하기상품 디테일 페이지장바구니 기능 구현하기📁public ├── products.json 📁src ├── App.tsx ├── index.tsx ├── 📁app │ └── store.ts ├── 📁components │ ├── Nav.tsx │ └── Products.tsx ├── 📁features ├──── 📁products │ └── productsSlice.ts ├──── 📁cart │ └── cartSlice.ts ├── 📁pages │ ├── Cart.tsx │ └── Detail.tsx productsSlice.ts : 상품 데이터 관리 (데이터 가져오기)// Product 타입 정의 export interface Product { category: string; name: string; price: number; description: string; } // ProductsState 타입 정의 interface ProductsState { items: Product[]; // 배열로 설정 status: 'idle' | 'loading' | 'succeeded' | 'failed'; } // 초기 상태 설정 const initialState: ProductsState = { items: [], // 빈 배열로 시작 status: 'idle', };'idle': 초기 상태로, 데이터 요청이 시작되지 않았음을 나타냄'loading': 상품 데이터를 요청 중임을 나타낸다. 로딩 애니메이션이나 메시지를 표시할 수 있다.'succeeded': 상품 데이터 요청이 성공. 데이터 화면에 표시 가능'failed': 상품 데이터 요청이 실패. 오류 메시지를 표시 가능 초기 상태 설정 initialState: 리듀서에서 상태를 초기화하는데 사용 애플리케이션이 시작될 때 어떤 상태를 가지는지 정의. 데이터 요청이 이루어지기 전에는 빈 배열과 idle상태를 가지고 있다. 이후 데이터 요청이 진행되고, 성공적으로 완료되면 상태가 업데이트 됨 Thunk 함수 정의 : 비동기 데이터 가져오기export const fetchProducts = createAsyncThunk( 'products/fetchProducts', async () => { const response = await fetch('/products.json'); // public 폴더의 JSON 파일을 비동기로 가져옴 const data = await response.json(); // JSON 데이터를 파싱 return data; // Thunk는 이 데이터를 fulfilled 상태로 반환 } );createAsyncThunk 사용할 때 자동으로 생성되는 액션 타입'{sliceName}/{thunkName}/pending' → products/fetchProducts/pending'{sliceName}/{thunkName}/fulfilled' → products/fetchProducts/fulfilled'{sliceName}/{thunkName}/rejected' → products/fetchProducts/rejected items 상품을 여러 개의 상품을 저장할 수 있는 배열 fetchProducts 의 비동기 작업이 성공적으로 완료되었을 때,/public/products.json에서 가져온 상품 데이터를 저장. Redux 슬라이스 정의const productsSlice = createSlice({ name: 'products', //리듀서 이름 initialState, // 데이터 초기 reducers: {}, // 상태가 변하면 어떻게 실행될지 extraReducers: (builder) => { builder .addCase(fetchProducts.pending, (state) => { state.status = 'loading'; // 로딩 상태 처리 }) .addCase(fetchProducts.fulfilled, (state, action) => {//데이터 요청 성공 state.status = 'succeeded'; //상태 업데이트 state.items = action.payload; // 받아온 데이터를 items에 저장 }) .addCase(fetchProducts.rejected, (state) => { state.status = 'failed'; // 실패 상태 처리 }); }, })productsSlice = createSlice : 리듀서 함수 정의initialState 위에 설정한 초기 상태 items: [] / status: 'idle'state는 Redux Toolkit의 createSlice를 사용하여 정의된 리듀서 함수에서 전달되는 매개변수로, 현재 상태를 나타낸다. 다시 정리하는 코드 흐름 :이해한 내용을 바탕으로 리덕스 데이터 플로우를 그려 봤다.초기 상태 const initialState: ProductsState = {} 에서 status는 'idle'로 설정. 이 상태는 비동기 요청이 시작되기 전.비동기 요청 시작 Products.tsx의 useEffect 훅에서 status가 'idle'일 때 dispatch(fetchProducts())를 호출 →const fetchProducts = createAsyncThunk 실행.데이터 요청 fetchProducts thunk는 /products.json 파일에서 상품 데이터를 비동기로 가져온다.응답 처리 요청 성공하면, Redux의 상태가 fulfilled로 업데이트.최종 상태 이때 status는 'succeeded'로 바뀌고, items 배열에는 JSON 파일에서 가져온 상품 목록이 저장됨. Products.tsx // 컴포넌트가 처음 마운트될 데이터 비동기 요청 useEffect(() => { if (status === 'idle') { dispatch(fetchProducts()); } }, [dispatch, status]); const filteredProducts = products.filter((product) => { if (selectedCategory === '모두') return true; if (selectedCategory === '전자기기') return product.category === 'electronics'; if (selectedCategory === '쥬얼리') return product.category === 'jewelry'; if (selectedCategory === '남성의류') return product.category === 'men'; if (selectedCategory === '여성의류') return product.category === 'women'; return false; }); 선택된 카테고리 따라 필터링 된 제품 리스트를 반환해준다. 원래는 spread operator로 데이터를 반환했는데 2주차때 작성했던 발자국을 보니 filter도 원본 배열을 수정하지 않고 새로운 배열을 반환해주는 메서드라 filter를 사용했다. cartSlice.ts : 장바구니 상태 관리 (상태 변경 및 업데이트)// cart 타입 정의 interface CartItem { id: string; name: string; category: string; price: number; quantity: number; } //CartState 타입 정의 interface CartState { items: CartItem[]; } //초기 상태 const initialState: CartState = { items: [], }; const cartSlice = createSlice({ name: 'cart', initialState, reducers: { addItem: (state, action: PayloadAction) => { // 장바구니에 아이템 추가 state는 initialState const existingItem = state.items.find( (item) => item.id === action.payload.id ); // 기존에 같은 아이템이 있는지 찾기 if (existingItem) { existingItem.quantity += 1; // 이미 존재하면 + 1 } else { // 존재하지 않으면 새로 추가하고 수량은 1로 설정 state.items.push({ ...action.payload, quantity: 1 }); } }, increaseQuantity: (state, action: PayloadAction) => { // 특정 아이템의 수량 1 증가 const item = state.items.find((item) => item.id === action.payload); if (item) item.quantity += 1; // 해당 아이템이 있으면 수량을 증가시킴 }, decreaseQuantity: (state, action: PayloadAction) => { // 특정 아이템의 수량 1 감소 const item = state.items.find((item) => item.id === action.payload); if (item && item.quantity > 1) item.quantity -= 1; // 아이템이 있고 수량이 1 이상일 때만 감소 }, removeItem: (state, action: PayloadAction) => { // 특정 아이템을 장바구니에서 제거 state.items = state.items.filter((item) => item.id !== action.payload); // 해당 아이템의 id와 일치하지 않는 아이템들로 배열을 갱신 }, }, }); export const { addItem, increaseQuantity, decreaseQuantity, removeItem } = cartSlice.actions; // 정의한 리듀서에 해당하는 액션 생성자들을 export export default cartSlice.reducer;* productsSlice는 데이터를 가져와 저장하는 용도로 상태를 직접 변경하지 않기 때문에 reducers 값이 비어있지만 cartSlice는 사용자의 동작에 따라 상태가 동적으로 변하기 때문에 reducers를 사용한다. 상품 클릭 시 /detail 페이지 이동 + 상품 장바구니 추가 기능 const { id } = useParams(); // URL에서 id 파라미터 가져오기 const dispatch = useDispatch();상품을 클릭하면 상품 페이지를 보여준다. const product = useSelector((state: RootState) => state.products.items.find((item) => item.id === id) );useParams를 통해 URL의 id 파라미터를 가져온다.Redux의 useSelector를 사용해 products 슬라이스에서 해당 상품(id)을 검색 → 일치하는 데이터 가져옴 const isInCart = useSelector((state: RootState) => state.cart.items.some((item) => item.id === id) );useSelector를 통해 cart 슬라이스에서 해당 상품이 장바구니에 있는지 확인some() 메서드를 사용해 장바구니에 같은 id의 상품이 있는지 여부를 반환 {/* 장바구니에 있는 경우 텍스트 변경 */} {isInCart ? ( 이미 장바구니에 담긴 상품 ) : ( 장바구니 추가 )} 장바구니로 이동 const handleAddToCart = () => { const cartItem = { ...product, quantity: 1 }; // quantity 추가 dispatch(addItem(cartItem)); // Redux에 추가 }; 장바구니 추가 핸들러quantity 속성을 1로 설정하여 Redux 상태에 저장 dispatch를 통해 addItem 액션을 호출해 장바구니 상태를 업데이트 /cart 장바구니 페이지const dispatch = useDispatch(); const cartItems = useSelector((state: RootState) => state.cart.items);Redux 스토어의 cart.items 배열을 가져온다. dispatch(decreaseQuantity(item.id))}> - dispatch(increaseQuantity(item.id))}> + dispatch(removeItem(item.id))}>삭제수량조절 & 삭제 버튼수량 버튼은 수량이 1 이하로 감소하지 않도록 cartSlice에서 설정한다.if (cartItems.length === 0) { return ( 장바구니가 비어있습니다. ); }아이템이 하나도 없을 때는 장바구니가 비었다는 문구를 보여준다. 작성할 때는 코드가 다 까만 배경이었는데 저장하고 나니까 이상하게 보인다 😥 마지막 제시간에 제출하기 위해 급하게 하다 보니 이미지도 넣지 못했다. 상품 목록 불러올 때 일부러 지연 시킨 다음에 사진이 로드 되기 전 UI도 구현하는 부분이랑 cart hover 했을 때 장바구니 아이템 보여주는 부분도 생략했다. 스터디는 이렇게 제출하지만 따로 추가해보려고 한다.
워밍업클럽
2024. 10. 15.
0
[인프런 워밍업 스터디 클럽 2기 FE] 3주차 과제 - 퀴즈 앱 만들기
퀴즈앱 만들기github : 11-quiz 기능목록퀴즈 풀었을 때 → 정답버튼 띄우기 → 답 확인 기능 플로우/question 에서 문제 2개 랜덤 출력/state 에서 선택된 카테고리의 문제 랜덤 출력/quiz 에서 선택 카테고리 문제 출력 & 진행률 & 결과 보여주기 구현하기폴더구조📁public ├── 📁data │ └── questions.json 📁src/app ├── layout.tsx ├── page.tsx ├── 📁components │ └── Questions.tsx //문제 UI ├── 📁hooks │ └── useQuestionHandler.ts ├── 📁question │ └── page.tsx ├── 📁quiz │ └── page.tsx ├── 📁state │ └── page.tsx /components/Questions.tsx /question 페이지와 /state 페이지에서 쓰는 문제 스타일은 똑같아서 컴포넌트를 만들어서 가져다가 사용했다.지금 생각해보니 /quiz 도 비슷한데 class 타입을 다르게 만들어서 적용할걸 그랬나 싶다.interface QuestionProps { question: string; //문제 choices: string[]; //선택지 selectedAnswer: string; //선택한 답변 resultClasses: string[]; //결과 class isButtonActive: boolean; //버튼 활성화 여부 questionAreaClass: string; //질문 영역 class onChange: (event: ChangeEvent) => void; // input 핸들러 onCheckAnswer: () => void; // 정답 확인 호출 핸들러 questionIndex: number; // 문제 고유 index }Questions 컴포넌트에서 사용할 props를 정의한다.export const Question: React.FC = ({ question, choices, selectedAnswer, resultClasses, isButtonActive, questionAreaClass, onChange, onCheckAnswer, questionIndex, }) => { return ( {question} {choices.map((choice, choiceIndex) => ( ))} ); };문제 UI 구조는 이렇게 생겼다. 다 같이 쓰면 코드가 이상하게 써져 span, input, label,button 은 아래 따로 뺐다. 사용자가 답을 선택했을 때 답 앞에 동그라미로 이 답이 정답인지 오답인지 알려주는 용도이다.이런식으로 구분이 된다. id= choice-${questionIndex}-${choiceIndex}한 문제가 가지는 4개의 radio input은 다 같은 name을 가지고 있다. 같은 name을 가지는 요소끼리 하나의 그룹으로 취급되어 같은 그룹 내에서 하나의 라디오 버튼만 선택할 수 있다. 답변을 확인하세요. 그룹 내에서 하나의 input을 고르면 답을 확인하라는 버튼이 보여주고 버튼을 누르면 상위 div인 question-area와 span 태그에 class가 붙는다. 정답이면 correct / 오답이면 wrong 붙어 색으로 구분이 가능하다. /question : 문제 2개 랜덤 출력 페이지 const [questions, setQuestions] = useState([]); // 문제 배열 useEffect(() => { // JSON 파일에서 데이터 불러오기 fetch('/data/questions.json') .then((res) => res.json()) .then((data) => { // 수학 문제에서 랜덤으로 하나 선택 const randomMath = data.math[Math.floor(Math.random() * data.math.length)]; // 국어 문제에서 랜덤으로 하나 선택 const randomKorean = data.korean[Math.floor(Math.random() * data.korean.length)]; // 두 문제를 새로운 배열에 담기 setQuestions([randomMath, randomKorean]); });/public/data/questions.json에 있는 데이터를 가져와 랜덤으로 출력해준다.수학 1문제, 국어 1문제를 출력하도록 설정했다. /state : 선택 과목 문제 랜덤 출력/public/data/questions.json에 있는 데이터를 가져와 랜덤으로 출력해준다.선택한 카테고리의 문제 2개를 랜덤으로 출력한다. // 과목 선택 시 JSON 파일에서 해당 과목 문제를 2개 랜덤으로 선택 useEffect(() => { if (selectedSubject !== '') { setIsLoading(true); // 문제를 불러오는 동안 로딩 상태로 설정 resetState(); // 과목이 바뀔 때 상태 초기화 fetch(`/data/questions.json`) .then((res) => res.json()) .then((data) => { if (selectedSubject === 'math') { const randomMathQuestions = getRandomQuestions(data.math, 2); // 수학 문제 2개 선택 setQuestions(randomMathQuestions); } else if (selectedSubject === 'korean') { const randomKoreanQuestions = getRandomQuestions(data.korean, 2); // 국어 문제 2개 선택 setQuestions(randomKoreanQuestions); } setIsLoading(false); // 문제를 불러온 후 로딩 상태 해제 }); } }, [selectedSubject]); 두 페이지에서 사용하는 사용하는 핸들러는 같다./hooks/useQuestionHandler.ts 를 사용하고 있다. return ( {questions.map((question, index) => ( handleChange(event, index)} // 선택 핸들러 onCheckAnswer={() => checkAnswer(index)} // 정답 확인 핸들러 questionIndex={index} // 질문 인덱스 전달 /> ))} );Question 컴포넌트에서 넘어오는 정보로 UI를 핸들링 한다.{ "id": 1, "question": "1 + 1은?", "choices": ["1", "2", "3", "4"], "answer": 1 },만약 내가 이 데이터 문제에서 4번째 답을 골랐다면useQuestionHandler의 handleChange 함수가 호출된다. const handleChange = ( event: ChangeEvent, index: number ) => { const selectedValue = event.target.value; //input 의 value 값인 4를 가져옴 const newSelectedAnswers = [...selectedAnswers]; newSelectedAnswers[index] = selectedValue; // 선택한 답변 저장 setSelectedAnswers(newSelectedAnswers); /* newSelectedAnswers 배열을 복사하여 선택한 값으로 현재 인덱스(여기서는 3)의 값을 업데이트 selectedAnswers[3]에 "4"가 저장된다 */ const newIsButtonActive = [...isButtonActive]; //버튼 활성화 newIsButtonActive[index] = true; setIsButtonActive(newIsButtonActive); /* 버튼 활성화 상태를 관리하는 배열 newIsButtonActive를 복사하여 현재 질문 인덱스의 값을 true로 설정 */ // 정답 확인 후 사용자가 다시 다른 답을 선택했을 때 class초기화 const resetResultClasses = new Array(questions[index].choices.length).fill(''); setResultClasses((prev) => { const updatedResultClasses = [...prev]; updatedResultClasses[index] = resetResultClasses; return updatedResultClasses; }); // question-area 클래스 초기화 setQuestionAreaClasses((prev) => { const updatedQuestionAreaClasses = [...prev]; updatedQuestionAreaClasses[index] = ''; // 초기화 return updatedQuestionAreaClasses; }); }; 위의 코드 외에 useQuestionHandler 에는 UI 상태 초기화 하는 함수, span에 class를 추가해 정답표시를 하는 함수도 있다. 상태 초기화 하는 함수는 나중에 넣었는데 /state 에서 수학 문제를 풀고나서 국어 문제를 호출했을 때 기존 UI class가 남아있어서 class를 제거해주는 함수를 추가로 넣었다. 코드를 더 깔끔하고 효율적으로 써보려고 컴포넌트와 훅을 분리 했는데 넘기고 받는 값이 많다 보니까 내가 쓴 코드인데도 헷갈렸다. 더 깔끔하게 쓰는 방법이 있는지.. 고민해봐야겠다. /quiz : 선택 과목 문제 출력 & 결과 보여주기/quiz 페이지에서 사용하는 상태관리const [selectedSubject, setSelectedSubject] = useState(''); // 선택한 과목 상태 const [questions, setQuestions] = useState([]); // 가져온 문제 상태 const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); // 현재 문제 인덱스 const [selectedAnswer, setSelectedAnswer] = useState(null); // 선택한 답 const [correctAnswersCount, setCorrectAnswersCount] = useState(0); // 맞은 답 개수 const [showQuiz, setShowQuiz] = useState(false); // 퀴즈 시작 여부 const [showResult, setShowResult] = useState(false); // 결과 화면 표시 여부 여기도 뭔가 많다.. 카테고리를 선택하고 테스트 버튼을 누르면 /state 페이지와 같이 문제를 불러오고 화면에 띄워준다. 문제는 하나씩 띄우고 다음 버튼을 눌렀을 때 넘어가게 만들었다. 마지막 문제일 때는 '다음' 이라는 텍스트가 아니라 '결과 보기' 텍스트를 띄운다. 그리고 상태 관리에 저장 된 값을 가져와 결과를 표시해준다. nextjs도 TypeScript에도 익숙하지 않아서 미션 하는데 시간이 생각보다 오래 걸렸다.앞으로는 자바스크립트보다 타입스크립트를 써서 익숙해져야 겠다는 생각이 든다.
워밍업클럽
2024. 10. 13.
0
[인프런 워밍업 스터디 클럽 2기 FE] 3주차 과제 - 포켓몬 도감 앱 만들기
포켓몬 도감 앱github : 10-pokeapi 기능목록PokéAPI를 사용하여 포켓몬 목록과 세부 정보를 요청하고 화면에 표시더보기 버튼 눌렀을 때 추가 데이터 요청포켓몬 고유 ID(숫자)를 사용하여 이전/다음 페이지 구현포켓몬의 타입에 따른 데미지 상성 관계를 모달 창으로 제공 구현하기띠부띠부씰 느낌으로 css를 넣어봤다.폴더구조📁src ├── App.js ├── App.css ├── 📁components │ ├── Modal.js │ ├── Nav.js │ ├── PokemonList.js │ └── SearchInput.js ├── 📁hooks │ ├── useFetchPokemonData.js │ └── useFetchPokemonDetail.js ├── 📁pages │ ├── Detail.js │ └── Main.js메인에서 추가 데이터 요청하기PokemonList.js 과 useFetchPokemonData.js 훅useFetchPokemonData.js : 페이지 번호에 따라 Pokémon 데이터를 가져오는 역할export const useFetchPokemonData = (pageNumber) => { const [pokemonData, setPokemonData] = useState([]); const [isLoading, setIsLoading] = useState(false); useEffect(() => { const fetchPokemonData = async () => { setIsLoading(true); try { const promises = []; // 한 페이지당 20개의 포켓몬 데이터 가져오기 const start = (pageNumber - 1) * 20 + 1; // 예: 1 페이지면 1~20, 2페이지면 21~40 const end = pageNumber * 20; for (let i = start; i response.data); setPokemonData((prevData) => [...prevData, ...newData]); // 기존 데이터에 추가 } catch (error) { console.error('포켓몬 데이터를 가져오는 중 오류 발생:', error); } finally { setIsLoading(false); } }; fetchPokemonData(); }, [pageNumber]); return { pokemonData, isLoading }; };상태관리 :pokemonData: 가져온 Pokémon 데이터 배열을 저장isLoading: 데이터 로딩 중 여부를 보여준다. (true 또는 false) 데이터 가져오기 :useEffect 훅을 사용하여 pageNumber가 변경될 때마다 데이터를 가져온다.fetchPokemonData라는 비동기 함수를 정의하고, 여기서 Pokémon 데이터를 API를 통해 가져옴1. const { pokemonData, isLoading } = useFetchPokemonData(pageNumber); 훅에 pageNumber = 1 전달2. fetchPokemonData 함수 내에서const start = (pageNumber - 1) * 20 + 1; // 예: 1 페이지면 1~20, 2페이지면 21~40 const end = pageNumber * 20;start 계산 : pageNumber 가 1일때 :첫 번째 페이지의 첫 번째 Pokémon ID가 1임을 나타낸다.end 계산 : 첫 번째 페이지의 마지막 Pokémon ID가 20임을 나타낸다.3. 데이터 요청for (let i = start; i baseUrl(axios) + fetchPokemonById (requests) → https://pokeapi.co/api/v2//pokemon/${id}for 루프를 통해 Pokémon 데이터를 가져오기 위해 API에 요청하고 const promises = []; 배열에 데이터 추가함4. 데이터 수신 및 상태 업데이트const responses = await Promise.all(promises); const newData = responses.map((response) => response.data); setPokemonData((prevData) => [...prevData, ...newData]); responses 모든 Promise가 이행되면, 이행된 결과를 포함하는 배열을 반환, responses는 20개의 응답 객체를 포함newData responses 배열에서 각 응답의 실제 데이터 부분을 추출setPokemonData() 기존 데이터에 새 데이터 추가 PokemonList.js더보기 데이터 요청const handleLoadMore = () => { setPageNumber((prevPageNumber) => prevPageNumber + 1); };사용자가 '더보기' 버튼을 클릭하면 handleLoadMore 함수가 실행 이 함수에서는setPageNumber를 호출fetchPokemonData 함수 내에서 start와 end 값을 다시 계산start = (2 - 1) * 20 + 1 = 21end = 2 * 20 = 40{!isLoading ? ( 더보기 ) : ( Loading... )}처음에는 const [isLoading, setIsLoading] = useState(false); 표시 → setIsLoading(true); 상태 업데이트isLoading이 true일 경우 'Loading...' 메시지를 표시하고, false일 경우 '더보기' 버튼을 보여준다. 포켓몬 타입별 색상코드 적용..bg-normal { background-color: #949495; }api데이터로 타입을 받아서 bg-${type} 형식으로 class지정.css에서 타입별 색상 코드를 적용해 놓는다. 포켓몬 넘버 3자리 수로 변경 1 → 001{String(pokemon.id).padStart(3, '0')} 포켓몬 검색 기능검색기능export default function SearchInput() { const [searchTerm, setSearchTerm] = useState(''); const navigate = useNavigate(); const handleSearch = () => { if (searchTerm.trim()) { navigate(`/${searchTerm.toLowerCase()}`); } }; return ( setSearchTerm(e.target.value)} placeholder='포켓몬 이름을 입력하세요' /> 검색 ); }포켓몬 이름을 입력하고 검색 버튼을 누르면 해당 이름의 페이지로 이동한다. 포켓몬 영문 이름은 소문자이기 때문에 toLowerCase() 를 사용해서 입력된 값이 소문자로 넘어가게 한다. trim()으로 있을지 모르는 공백 제거도 해준다. 디테일 페이지와 모달Detail.js 컴포넌트와 useFetchPokemonDetail.js 훅Detail.js 컴포넌트는 특정 포켓몬의 세부 정보를 보여주는 컴포넌트.포켓몬 이름을 URL에서 가져와 해당 포켓몬의 데이터를 요청하고, 포켓몬 설명 및 타입에 따른 데미지 관계 정보를 함께 표시한다.export default function Detail() { const { id: pokemonName } = useParams(); const { pokemon, description, damageRelations } = useFetchPokemonDetail(pokemonName); const [isModalOpen, setIsModalOpen] = useState(false); const navigate = useNavigate(); if (!pokemon) { return Loading...; } // 모달 열기/닫기 기능 const handleOpenModal = () => setIsModalOpen(true); const handleCloseModal = () => setIsModalOpen(false); // 이전 포켓몬 데이터로 이동 (ID로 요청) const handlePrev = () => { const prevId = pokemon.id - 1; if (prevId >= 1) { axios.get(requests.fetchPokemonById(prevId)).then((response) => { setIsModalOpen(false); navigate(`/${response.data.name.toLowerCase()}`); }); } }; //다음 포켓몬 const handleNext = () => {/*생략*/} return ( //생략 {/* 모달이 열렸을 때만 렌더링 */} {isModalOpen && ( )} ) };1. 포켓몬 이름 가져오기 useParams 사용const { id: pokemonName } = useParams();PokemonList.js 에서 onClick={() => navigate(`/${pokemon.name}`)} URL에 있는 포켓몬 이름을 가져와서 id 값으로 쓴다. 2. 훅 useFetchPokemonDetail.js 포켓몬 이름으로 데이터 요청const { pokemon, description, damageRelations } = useFetchPokemonDetail(pokemonName); 해당 포켓몬의 정보 (pokemon), 설명 (description), 데미지 관계 (damageRelations)를 받아온다. 3. 로딩 상태 처리포켓몬 데이터가 아직 로드되지 않았다면 Loading... 메시지를 표시한다. if (!pokemon) { return Loading...; } 4. 모달 열기/닫기: const [isModalOpen, setIsModalOpen] = useState(false); // 모달 열기/닫기 기능 const handleOpenModal = () => setIsModalOpen(true); const handleCloseModal = () => setIsModalOpen(false);isModalOpen 상태를 사용하여 모달의 열림/닫힘을 관리한다. 5. 이전/다음 포켓몬으로 이동: const handlePrev = () => { const prevId = pokemon.id - 1; if (prevId >= 1) { axios.get(requests.fetchPokemonById(prevId)).then((response) => { setIsModalOpen(false); navigate(`/${response.data.name.toLowerCase()}`); }); } };API를 통해 받아온 포켓몬의 고유 ID 값 사용.handlePrev 함수는 현재 포켓몬의 ID에서 1을 빼서 이전 포켓몬으로 이동한다.ID가 1 이상일 경우, 이전 포켓몬의 데이터를 요청하고, 응답받은 이름으로 URL을 변경. 다음 이동 함수도 같은 내용. useFetchPokemonDetail 훅 : 포켓몬의 이름을 받아 해당 포켓몬의 세부 정보, 설명, 데미지 관계 정보를 가져온다.export const useFetchPokemonDetail = (pokemonName) => { //생략 return { pokemon, description, damageRelations }; }; 1. 상태관리 const [pokemon, setPokemon] = useState(null); const [description, setDescription] = useState(''); const [damageRelations, setDamageRelations] = useState(null);pokemon: 포켓몬의 기본 데이터를 저장description: 포켓몬의 설명을 저장damageRelations: 포켓몬 타입에 따른 데미지 관계 데이터를 저장 2. 포켓몬 데이터 요청:useEffect(() => { const fetchPokemonDetail = async () => { try { // 포켓몬 기본 정보 요청 const response = await axios.get( requests.fetchPokemonByName(pokemonName) ); setPokemon(response.data); // 포켓몬 설명 const speciesResponse = await axios.get( requests.fetchPokemonSpecies(pokemonName) ); const flavorTextEntry = speciesResponse.data.flavor_text_entries.find( (entry) => entry.language.name === 'en' ); setDescription( flavorTextEntry ? flavorTextEntry.flavor_text : 'No description available.' ); // 포켓몬 타입에 따른 데미지 관계 가져오기 if (response.data.types.length > 0) { const typeName = response.data.types[0].type.name; // 첫 번째 타입 선택 const typeResponse = await axios.get( requests.fetchPokemonType(typeName) ); setDamageRelations(typeResponse.data.damage_relations); // 데미지 관계 설정 } } catch (error) { console.error('포켓몬 데이터를 가져오는 중 오류 발생:', error); } }; fetchPokemonDetail(); }, [pokemonName]);useEffect는 pokemonName이 변경될 때마다 실행된다.axios.get을 사용하여 포켓몬 이름으로 기본 데이터를 요청하고, setPokemon으로 상태를 업데이트.가져온 데이터에서 영어 설명(flavor_text_entries)을 찾아서 설명 상태에 저장 만약 설명이 없으면 기본 메시지를 설정한다.포켓몬의 타입 정보가 있으면 첫 번째 타입의 이름을 사용하여 해당 타입에 대한 데미지 관계 정보를 요청하고 이 데이터를 damageRelations 상태로 저장.pokemon, description, damageRelations 데이터를 반환하여 Detail.js에서 사용할 수 있게 한다.
워밍업클럽
2024. 10. 13.
0
[인프런 워밍업 스터디 클럽 2기 FE] 3주차 발자국
따라하며 배우는 리액트 A-Z 마무리섹션 7~8 TDD 리액트 테스트 경험하기테스트 코드를 작성하면 좋은 점 : 많은 기능을 테스트 하기에 소스 코드에 안정감이 부여된다. 실제 개발하면서 많은 시간이 소요되는 부분은 디버깅 부분이라 TDD를 사용하면 디버깅 시간이 줄어든다. 코드를 더욱 신중하게 짤 수 있다.React Testing Library : create-react-app로 생성된 프로젝트는 이미 지원하는 라이브러리. DOM Node를 테스트하기 위한 매우 가벼운 솔루션이다. 섹션 9 Next.js와 TypeScript- Next.js 대해 알아보기리액트의 SSR을 쉽게 구현할 수 있게 도와주는 간단한 프레임워크리액트로 개발할 때, SPA를 사용하여 CSR(Client-Side Rendering)을 적용하면 장점이 많지만, 단점도 있다. 그 중 하나가 SEO 최적화 문제. CSR 방식에서는 첫 페이지에서 빈 HTML 파일을 불러오고, 자바스크립트 파일을 해석해 화면을 구성하기 때문에 검색 엔진에 제대로 노출되지 않을 가능성이 크다. → 리액트에서도 SSR을 지원하지만 구현하기에 복잡하다.Pre-rendering : 모든 페이지에 대한 HTML을 미리 생성하여 클라이언트 사이드에서 자바스크립트로 처리하기 전에 브라우저에 전달하는 방식. 검색 엔진 크롤러가 자바스크립트를 실행하지 않고도 완전한 HTML 콘텐츠를 확인할 수 있기 때문에 SEO(Search Engine Optimization) 최적화에 매우 유리. (페이지가 미리 렌더링되므로 검색 엔진이 더 쉽게 콘텐츠를 인덱싱하고, 검색 순위가 개선될 가능성이 커진다.)Data FetchinggetStaticProps (정적 생성) : 빌드 시에 데이터를 미리 가져와서 HTML을 생성하는 방식. 정적 콘텐츠를 사전에 생성 → 페이지 로딩 속도가 빠르고 서버 부하가 적다. 주로 콘텐츠가 자주 변경되지 않는 경우에 적합getServerSideProps(서버사이드 렌더링) : 페이지 요청 시 서버에서 데이터를 가져와 동적으로 HTML을 생성하는 방식. 실시간 데이터가 필요한 경우나 사용자별로 다른 데이터를 제공해야 할 때 유용. (ex. 동적 라우팅이 필요한 경우 (pages/post/[id].js)에 활용.)클라이언트 사이드 데이터 페칭 (Client-side Data Fetching) : 페이지가 로드된 후 클라이언트에서 데이터를 가져오는 방식. Server Side Rendering(SSR)과 달리, 초기 HTML이 로드된 후 추가적인 API 요청을 통해 데이터를 온다. 실시간 업데이트, 인터랙티브 콘텐츠에 사용.fallbackgetStaticPaths로 미리 정의된 경로 외에는 모두 404 페이지로 처리getStaticPaths로 미리 생성되지 않은 경로는 첫 번째 요청 시 "fallback" 상태가 표시되며, 해당 페이지가 서버에서 동적으로 생성. - TypeScript 대해 알아보기타입스크립트(TypeScript)는 자바스크립트에 타입을 추가하여 더 안전하고 유지보수하기 쉬운 코드를 작성할 수 있게 만든 확장된 프로그래밍 언어입니다. 자바스크립트의 모든 기능을 포함하면서도, 정적 타입을 지원해 코드 작성 중에 타입 오류를 미리 확인할 수 있습니다.타입스크립트의 타입any : 어떤 타입이든 할당할 수 있는 타입Union : 둘 이상의 타입을 사용할 때 사용하는 타입Enum : 값들의 집합에 이름을 붙인 타입. enum은 선언된 후 수정할 수 없으며, 값은 숫자 또는 문자열만 허용Void : 함수에서 반환값이 없을 때 사용되는 타입. 일반적으로 undefined나 null 값을 가질 수 있지만 의미상 값이 없는 경우를 나타냄.Never : 절대 발생하지 않는 값을 나타내는 타입. 어떤 값도 가질 수 없다. - Nextjs13Static Data fetching next에서 fetch 함수는 는 자동으로 데이터를 가져오고 캐시한다.Refresh on every request 데이터가 캐시가 안되게 하고 모든 리퀘스트마다 다시 가져올 수 있게 한다. getServerSideProps유사Revalidating Data 캐시된 데이터를 일정 시간 간격으로 재 검증하는 하는 방법fenerateStaticParams generateStaticParams는 getStaticPaths와 유사하게 동작하며 특정 페이지에서 정적으로 생성될 때 경로를 지정Server Action 서버 액션을 통해 클라이언트 컴포넌트에서 서버 작업을 바로 처리할 수 있다. 자바스크립트가 비활성화된 환경에서도 서버 액션을 사용할 수 있도록 지원Form Action : action={} 속성은 HTML 폼에서 서버로 데이터를 전송할 때 사용Parallel Routes : 여러 경로를 동시에 처리하고, 페이지 간의 독립적인 렌더링을 기능.default.tsx : 페이지에서 기본적으로 제공되는 파일. 특정 경로에 대한 기본적인 레이아웃을 제공.[...catchAll] : 어떤 경우로 들어오더라도 이 컴포넌트가 렌더링 된다.Image CommponentIntercepting Routes : 바뀐 경로로 모달을 띄울 수 있음.Partial Pre-rendering (PPR) : 정적 컴포넌트와 동적 컴포넌트 함께 사용. ex) 검색input과 검색 결과 창Image Component : image 태그를 사용하면 자동으로 제공되는 props가 있다. 빌드할 때 이미지를 최적화 해줌placeholder="blur" 원본이미지가 다운로드 되기전에 블러된 저해상도 이미지가 보임blurDataUrl : 로딩될동안 미리 보여줄 이미지 그냥 색상 코드도 입력 가능Remote Images 외부에서 가져오는 이미지는 widthl, height,를 줘야한다. 그리고 config 에서 remotePatterns도 설정해줘야 함이미지를 가져오는 동안에 들어갈 높이와, 넓이를 쓰면 레이아웃 쉬프트(Layout Shift) 방지할수있다.이미지랑 높이를모르면? fill 사용하면 부모높이를 전체 차지Responsive : 반응형 이미지 처리sizes 값은 next.js 이미지에 의해서 자동으로 생성된 이미지들 중에서 어떤 이미지를 다운로드할지 브라우저가 결정하는데 도움을 준다.next13의 Image Commponent 부분이 좋았다. 아무래도 이미지는 웹에서 신경써야 할 부분이 많은데 알아서 이미지를 최적화 해준다거나.. blur기능이라던가 다음에 next에서 이미지를 써야 한다면 img 말고 컴포넌트로 써봐야겠다. 섹션10 리액트 18버전 Automatic Batching (자동 배칭) : 여러 상태 업데이트를 하나의 그룹으로 묶어 한 번에 렌더링을 처리. 기존에는 이벤트 핸들러에서만 자동 배칭이 적용되었지만, React 18에서는 API 요청이나 타임아웃 함수에서도 이 기능이 확장되었다. 불필요한 여러 번의 렌더링을 줄여 성능이 향상.Suspense on the Server : 서버 사이드 렌더링에서 Suspense를 사용할 수 있게 됨. Suspense는 컴포넌트를 더 작은 독립적인 단위로 분할. 사용자가 모든 데이터를 기다리지 않고 일부 콘텐츠를 더 빠르게 볼 수 있다.Transitions (트랜지션) : Transition을 도입하여 상태 업데이트의 우선순위를 관리할 수 있게 됨. 즉각적인 응답이 필요한 업데이트와 그렇지 않은 업데이트 분리 가능. ex) 검색창: 사용자가 검색어를 입력하면 즉각적으로 업데이트가 필요하다.검색 결과 창: 검색 결과를 로드하는 동안 트랜지션을 적용해 부드럽게 업데이트할 수 있다. 사용자는 검색어 입력에는 즉각 반응을 보고, 결과는 천천히 업데이트되더라도 매끄럽게 경험할 수 있다. 섹션 11 리덕스에 대해서리덕스는 자바스크립트 애플리케이션을 위한 상태 관리 라이브러리다.리덕스(Redux)의 데이터 플로우: 액션(Action)이 발행되면 → 리듀서(Reducer) 호출 → 스토어(Store) 내부 상태가 업데이트 → 새로운 상태에 따라 컴포넌트가 다시 렌더링Sub Reducer: 모든 상태 관리를 하나의 리듀서에서 처리하는 것이 비효율적이고 유지 보수가 어려워지기 때문에, 여러 작은 리듀서들로 나누어 상태를 관리하는 방식 combineReducers로 합칠 수 있다.Redux Middleware: 액션(Action)이 디스패치(Dispatch)된 후 리듀서에 도달하기 전에 중간에서 지정된 작업을 실행할 수 있게 해주는 중간자. 로깅, 충돌 보고, 비동기 API와 통신, 라우팅 등을 위해 리덕스 미들웨어를 사용한다.Redux Thunk 사용하는 이유 : 리덕스에서 비동기 작업을 처리하기 위해서Redux Toolkit: 리덕스 툴킷은 리덕스 로직을 작성하기 위한 권장 접근 방식이다.리액트의 불변성 지키기 처럼 리덕스도 state를 직접적으로 변경하지 않는다.리덕스 툴킷 APIs :빌더 콜백 (Builder Callback) : addCase 특정 액션 타입에 맵핑된 리듀서 함수 추가, addMatcher 새로 들어오는 모든 액션에 대해서 주어진 패턴과 일치하는지 확인하고 리듀서를 실행, addDefaultCase 다른 케이스 리듀서나 매처 리듀서가 실행되지 않았다면 기본 케이스 리듀서가 실행 된다맵 오브젝트(Map Object) : Initial State 리듀서를 처음 호출할 때 사용하는 초기 값, Action Map 액션 타입이 케이스 리듀서에 맵핑되어 있는 객체, Case Reducer 이 작업에 대해 리듀서 및 매처 리듀서가 실행되지 않는 경우 실행되는 기본 케이스 리듀서 섹션 12 도커 체험하기어떠한 프로그램을 다운 받는 과정을 간단하게 만들기 위해서.컨테이너를 사용하여 응용프로그램을 더 쉽게 만들고 배포하고 실행할 수 있도록 설계된 도구. 컨테이너 기반의 오픈소스 가상화 플랫폼. 섹션 13 리액트 19use() Hooks : 비동기 요청으로 데이터를 가져오는 새로 나온 Hook. Actions : 클라이언트-서버 간 데이터 처리를 간소화.useFormStatus : 폼의 상태를 추적하는 데 사용되는 훅useActionState : Action을 편하게 쓰기 위해 도입useOptimistic: 새로 도입된 훅. 인터페이스가 즉각적으로 반응하는 것처럼 보이게 할 수 있다.Mata tags: 과거에는 수동으로 삽입하거나 라이브러리를 사용했으나 이제 컴포넌트 자체에서 바로 사용할 수 있다.React Server Component: 서버에서 렌더링되고 클라이언트에 전송되는 리액트 컴포넌트. 3주차 과제포켓몬 도감 앱 만들기 (Post Link)github : Link 퀴즈 앱 만들기 (Post Link)github : Link 리덕스를 이용한 쇼핑몰 만들기 (Post Link)github : Link 3주차 회고시간이 정말 빠르다. 벌써 3주 차 회고를 하고 있다. 스터디 신청한지 얼마 안 된 거 같은데 벌써 3주나 지났다니..처음 목표는 강의 100% 수강과 모든 미션 완료였다. 10월에는 휴일도 좀 있어서 할 수 있을지도 모른다고 생각했는데.. 모든 걸 다 하려니 시간이 모자라긴 했다. 3주 차에 있는 Day 12 채팅 앱, Day 13 Note 앱 미션은 하지 못해서 아쉽다. 3주 차 미션 중에서는 이 두 미션이 재미있어 보였고 꼭 하고 싶다고 생각했는데, 시간이 오래 걸릴 것 같아 일단 제외했다. 완료하지 못한 미션은 스터디가 끝나고 개인적으로 진행할 생각이다.자바스크립트 부분도 코치님한테 받았던 코드랑 작성했던 코드를 비교해서 보고 싶었는데 3주차에는 시간이 없어서 스터디 끝난 후로 미뤘다.마지막으로 진행했던 쇼핑몰 미션은 익숙하지 않은 타입 스크립트에 리덕스를 함께 사용하다 보니 생각보다 시간이 오래 걸렸다. 중간 점검 때 코치님이 리덕스 어렵다고 하셨는데 정말 어려웠다. 그래도 어떻게 미션은 완료했다.시간이 부족해 아쉬운 점도 있지만 퇴근 후 집에 와서 3주 동안 바쁘게 집중할 수 있었던 경험은 즐겁고 유익한 시간이었다. 남은 10월에는 위에서 말한 것처럼 완료 못한 미션이나 코드 비교를 하며 공부하는 시간을 가질 생각이다.커리큘럼 마지막 날인 오늘 이렇게 3주 차 발자국을 마무리한다.
워밍업클럽
2024. 10. 12.
0
[인프런 워밍업 스터디 클럽 2기 FE] 2주차 과제 - 디즈니 플러스 앱
로그인 전 화면 디즈니 플러스 앱GitHub : 09-disneypluGIF 파일 목록이 너무 커서 글에 올라가지 않아 md 파일에 동작하는 GIF를 추가해 두었다.기능 목록TMDb API 데이터 받아오기 검색 후 해당 아이템 클릭 시 영화 포스터 보여주기영화 클릭 시 모달 오픈슬라이드구글 로그인 구현하기TMDb API 데이터api로 데이터를 받아오는 방법은 강의에서 넷플릭스 앱 만들기에서 배웠던 걸 그대로 사용했다.axios.js 에서 baseURL을 설정하고 나머지 데이터를 받아올 주소도 requests에 설정해준다.Google OAuth2 구글 로그인구글 로그인 구현하는 방법은 검색하면 친절하게 설명되어 있는 블로그 글들이 많아 따라했다.api 키는 .env 파일에 담아서 커밋했을 때 깃허브에 올라가지 않게 한다.const [loggedIn, setLoggedIn] = useState(false); const [user, setUser] = useState(null); const CLIENT_ID = process.env.REACT_APP_GOOGLE_ID; const CLIENT_SECRET = process.env.REACT_APP_GOOGLE_PW; const REDIRECT_URI = 'http://localhost:3000'; const SCOPE = process.env.REACT_APP_GOOGLE_SCOPE;loggedIn : 사용자가 로그인 했는지 여부를 저장. 기본 값 falseuser : 사용자 정보 저장. 로그인 되면 사용자 정보가 저장된다.CLIENT_ID CLIENT_SECRET env 파일에 있는 클라이언트ID와 비밀번호를 가져온다.REDIRECT_URI : OAuth2 인증 후, 리다이렉션할 URL 로컬 개발 환경이라 http://localhost:3000으로 설정.로그인 const handleGoogleLogin = () => { const googleOAuthUrl = `https://accounts.google.com/o/oauth2/auth?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=${SCOPE}`; window.location.href = googleOAuthUrl; };사용자가 로그인 버튼을 클릭했을 때 호출하는 함수.googleOAuthUrl OAuth2 인증 URL을 생성하고, 그 URL로 이동. URL로 이동하면 Google 로그인 페이지가 열리고, 사용자가 Google 계정으로 로그인하게 된다.로그아웃 const handleLogout = () => { // 로그아웃 시 로컬 스토리지에서 사용자 정보 삭제 localStorage.removeItem('user'); localStorage.removeItem('access_token'); setUser(null); setLoggedIn(false); };로그아웃 버튼을 누르면 호출된다. 로컬스토리지에 저장된 사용자 정보와 액세스 토큰을 삭제하고 상태를 초기화하여 로그아웃 상태로 만든다.로그인 후 화면로그인할때 받은 유저 정보를 통해 오른쪽 상단에 구글 프로필 이미지를 넣어줬다.동영상 배너 {items.map((item, index) => ( handleMouseEnter(index)} onMouseLeave={() => handleMouseLeave(index)} > (videoRefs.current[index] = el)} playsInline loop muted data-testid='brand-set-video' > ))} 동영상/이미지를 다 넣으니 코드가 너무 길어져서 영상, 이미지 주소는 items라는 배열에 담아서 사용했다.const videoRefs = useRef([]); const handleMouseEnter = (index) => { if (videoRefs.current[index]) { videoRefs.current[index].play(); } }; const handleMouseLeave = (index) => { if (videoRefs.current[index]) { videoRefs.current[index].pause(); } }; handleMouseEnter, handleMouseLeave 마우스 이벤트를 사용해 hover 상태일 때 동영상을 재생시키고 일시 정지 시켰다. 동영상이 보였다 안 보이는 하는 건 css에서 opacity로 처리했다.처음에 다 만들고 동작 상태를 확인하다가 다른 페이지에서 메인으로 넘어왔을 때 로그인 상태가 유지가 되지 않아 유저 정보를 로컬 스토리지에 저장해서 구분했다. 로컬 스토리지에 이런 정보를 저장해도 되는 건지 모르겠지만.. 이런 로그인 연동에 대해 좀 더 알아봐야겠다.디즈니 메인 화면 보고 싶은데 볼 수가 없어서 한 달 정도 디즈니 플러스 결제를 했다. 스터디가 끝나면 재밌는 영화보는 시간을 가져야겠다.
2024. 10. 09.
0
[인프런 워밍업 스터디 클럽 2기 FE] 2주차 과제 - 예산 계산기
예산 계산기 만들기GitHub : 08-budget-calculato 기능 목록분류 / 지출 내역 / 비용 데이터를 받아 리스트 생성하기추가 / 수정 / 삭제 이벤트 → toasts 메세지 1초 띄우기총 비용 계산하기전체 삭제 기능 구현하기폴더구조📁src ├── App.js ├── App.css ├── 📁components │ ├── ExpenseForm.js │ ├── ExpenseList.js │ └── ExpenseLists.js데이터 구조 정하기expense: 항목, details: 지출 내역, amount: 비용,1. ExpenseForm : 데이터 전달데이터를 입력하고 제출을 누르면 App.js에 있는 handleSubmit 함수로 데이터 전달. handleSubmit 함수에서는 const updatedData = [...prev, newExpenseData]; 스프레드 연산자를 사용해 배열에 새 항목을 추가한다.React에서는 상태(state)를 직접 수정하지 않고 새로운 값을 반환하는 방식으로 상태를 업데이트해야 한다. 불변성을 유지하는 것이 중요한 이유는 React가 상태가 변할 때 렌더링을 다시 수행하는데, 상태가 변경된 것을 인식하기 위해서는 참조가 완전히 새로운 값으로 바뀌어야 하기 때문.강의에서 불변성에 대한 설명을 듣고 바로 기능을 만드니까 더 잘 기억에 남는다.2. toasts 메세지 조건부 렌더링하기const showMessageWithColor = (status) => { let color; let message; if (status === 'add') { message = '아이템이 생성되었습니다.'; color = '#f0faf6'; } else if (status === 'edit') { message = '아이템이 수정되었습니다.'; color = '#f0faf6'; } else if (status === 'delete') { message = '아이템이 삭제되었습니다.'; color = '#fee'; } setMessage(message); setMessageColor(color); setShowMessage(true); // 1초 후에 메시지 숨기기 setTimeout(() => { setShowMessage(false); }, 1000); };showMessage: 메시지를 표시할지 여부를 결정합니다. true 표시, false 숨김 처리message: 사용자에게 표시할 메시지 텍스트messageColor: 메시지의 배경색을 설정status 매개변수를 통해 메시지의 종류( 'add', 'edit', 'delete')를 구분한다.{showMessage && ( {message} )}showMessage가 true일 때만 메시지 박스가 렌더링style 속성을 사용하여 messageColor를 배경색으로 설정3. 총 지출 비용 계산const calculateTotalAmount = (data) => { return data.reduce((acc, item) => acc + item.amount, 0); };수정 버튼을 누르고 input을 수정할 때마다 값이 반영되어서 저장 버튼을 누르면 총 지출 비용을 계산하도록 함수로 따로 만들었다. 최종 값은 toLocaleString() 사용해 쉼표가 포함된 형식으로 바꿔준다.4. 항목 전체 삭제ExpenseList에 있는 전체 치우기 버튼을 눌러주면 기존 배열에 들어있던 데이터를 지우고 calculateTotalAmount() 에도 값을 전달해 0으로 초기화 시켜준다.강의에서 들었던 To-Do 앱과 그렇게 큰 기능 차이가 나지 않았기 때문에 어렵지는 않았다. 강의 내용을 복기하면서 비슷한 흐름으로 만들었다. 리액트의 불변성을 지키고 최적화를 잘 고려해야겠다.
워밍업클럽
2024. 10. 07.
0
[인프런 워밍업 스터디 클럽 2기 FE] 2주차 과제 - 타이핑 테스트
타이핑 테스트 앱 만들기GitHub : 07-typing-test 개요타이핑 테스트 기능 구현주어진 시간 내에 문장을 타이핑하고 입력 속도(WPM, CPM)와 정확도를 측정 필요한 기능타이핑된 값결과 팝업 보여주기 구현하기초반에 필요하다고 생각되는 부분들을 셋팅한다.typingInput.addEventListener('input', () => { const spans = printText.querySelectorAll('span'); const userInput = typingInput.value; // 사용자가 입력한 값 userInputArray = userInput.split(''); totalTypedCharacters = userInput.length; // 입력된 문자 수 // 두 배열을 비교하여 클래스 추가 for (let i = 0; i 처음에는 input 값을 실시간으로 받고 현재 타이핑 해야 하는 문장과 비교를 했었는데 생각과 달리 정확한 비교가 어려웠다. 그래서 현재 타이핑 해야 하는 글자와 사용자가 입력한 글자를 모두 배열의 요소로 저장하고 비교하는 방법으로 바꾸고 span 요소에 class를 추가해 complete / error 를 표시했다.// 오류 수 계산 function calculateErrors() { // 현재 문장의 오류 수 currentSentenceErrors = 0; // 현재 문장에서의 오류 수를 계산 for (let i = 0; i 원래는 오류 수 계산을 typingInput.addEventListener()의 for문에서 같이 처리했었는데, 다음 문장으로 넘어갈 때 오류 개수가 이상하게 업데이트 되는 바람에 함수로 분리하고 문장 별로 오류 수를 저장하고 업데이트하는 방식으로 변경했다. 오류 개수와 정확도는 화면에 실시간으로 반영되는 값이라 정확도 함수에 오류 수를 넘겨서 같이 업데이트 했는데 지금 보니까 그냥 같은 함수에서 처리해도 됐을 것 같다는 생각이 든다.function loadNextSentence() { // 현재 문장의 오류를 전체 오류에 누적 totalErrors += currentSentenceErrors; // 새로운 문장 현재 문장 오류 초기화 currentSentenceErrors = 0; currentSentenceIndex++; const nextSentence = TYPINGS[currentSentenceIndex]; renderTextWithSpans(nextSentence); typingInput.value = ''; userInputArray = []; typingInput.setAttribute('maxlength', currentTypingText.length); } // 엔터를 눌렀을 때 다음 문장 로드 typingInput.addEventListener('keydown', (_event) => { if (_event.key === 'Enter') { // 기본 엔터 입력 방지 _event.preventDefault(); if (userInputArray.length === currentTypingText.length) { if (currentSentenceIndex 다음 문장으로 넘어가는 함수에서는 textarea에 maxlength값을 넣어 타이핑해야 하는 글자 수 이상으로 타이핑하지 못하게 했다. enter을 눌렀을 때 다음 문장으로 바뀌는 방법으로 했더니 다음 문장으로 넘어갔을 때 textarea에 enter가 입력되면서 줄바꿈 처리가 되어서 _event.preventDefault(); 로 줄바꿈이 되지 않게 했다.function startTimer() { timerInterval = setInterval(() => { if (currentTime > 0) { currentTime--; timeElement.textContent = currentTime; } else { clearInterval(timerInterval); typingInput.disabled = true; showTypingResult(); } }, 1000); }마지막 문장까지 입력하고 enter을 누르면 결과 팝업이 뜨지만 처음 설정해둔 시간이 다 됐을 때도 결과 팝업이 뜬다.function initializeTypingTest() { calculateTotalCharacters(); const firstSentence = TYPINGS[0]; renderTextWithSpans(firstSentence); typingInput.setAttribute('maxlength', currentTypingText.length); errorsElement.textContent = INITIAL_ERRORS; timeElement.textContent = INITIAL_TIME; accuracyElement.textContent = INITIAL_ACCURACY; typingInput.value = ''; }초기 값들도 상수로 선언해서 화면을 초기화 하는 함수에서 사용했다.이번에는 과제를 하기 전에 자바스크립트 명명 규칙을 지켜서 해보기로 했다. 자바스크립트 미션 중에서 제일 오래 걸렸던 미션이다. 처음 구현했을 때 생각했던대로 작동하지 않아서 어떻게 만들어야할지 고민이 많았다. 계속 수정해가며 기능을 완성시키기는 했는데 이게 맞는지.. 어려웠다.
워밍업클럽
2024. 10. 06.
1
[인프런 워밍업 스터디 클럽 2기 FE] 2주차 발자국
2주차 회고2주차에 들은 강의들을 짧게 정리해본다. 섹션 8- 10 까지 자바스크립트 강의가 끝났다. 섹션 10이 제일 재밌었던 것 같다. 이론 강의도 좋지만 배운 이론을 이용해서 눈에 보이는 결과물이 생기는 게 제일 재밌는 것 같다. 따라하며 배우는 자바스크립트 A-Z 마무리Generator 함수 : 비동기 프로그래밍, 메모리의 효율성, 무한 시퀀스 생성, 제어 흐름 관리Design PatternSingleton Pattern : 클래스의 인스턴스를 하나만 생성단일 객체가 필요할 때 (예: 설정 관리, 로깅)Factory Pattern : 객체 생성 로직을 캡슐화, 특정 인터페이스를 통해 객체 생성객체 생성 로직이 복잡하거나 변경될 수 있는 경우 (예: 다양한 유형의 UI 요소를 생성)객체의 구체적인 생성 과정이 클라이언트에 노출되지 않아야 할 때.Mediator Pattern : 객체 간의 상호작용 중재하는 객체를 두어 객체 간의 의존성을 줄임객체 간의 상호작용이 복잡하고, 그 관계를 단순화하고 싶을 때 (예: UI 컴포넌트 간의 이벤트 조정)여러 객체가 서로 상호작용하지만, 서로의 구체적인 클래스에 의존하지 않아야 할 때.Observer Pattern : 체의 상태 변화에 따라 다른 객체에 알림을 보내는 패턴데이터 변경을 여러 곳에서 반영해야 할 때 (예: 뉴스 발행 시스템)Module Pattern : 데이터와 메서드를 캡슐화하여 외부에서 접근할 수 없도록 하여 코드의 네임스페이스를 보호전역 변수의 오염을 피하고, 코드의 재사용성을 높이고 싶을 때.관련된 기능이나 상태를 그룹화하여 코드의 구조를 명확하게 하고 싶을 때. 따라하며 배우는 리액트 A-Z리액트 시작. 리액트 강의를 들으면서 노트 기능을 활용해 봤는데 생각보다 좋았다. 강의를 들으면서 그때그때 해놓은 메모를 보니 복기하면서 발자국을 작성하는게 쉬워졌다. 섹션 2 / 리액트란?섹션 2에서는 는 리액트에 대한 개념을 익힐 수 있다.리액트는 프레임워크가 아닌 라이브러리여러개의 컴포넌트가 모여서 하나의 페이지를 이룬다.브라우저가 그려지는 원리와 가상 돔리액트 앱은 웹 브라우저에서 실행되는 코드라 Node.js와 직접적인 연관은 없지만 개발하는데 필요한 주요 도구들이 Node.js를 사용한다. 섹션 3~4 / 간단한 To-Do 앱 만들며 리액트 익히기SPA (single page application) 웹사이트의 모든 페이지를 하나의 페이지에 담아, 동적으로 화면을 전환HTML5의 History API를 사용하여, 실제로는 페이지를 이동하지 않지만 마치 다른 페이지로 이동한 것처럼 작동JSX Key 속성 이해하기리액트는 가상 돔을 이용해서 바뀐 부분을 찾는다.key 속성은 리스트나 컴포넌트가 렌더링될 때, 각 항목을 고유하게 식별하는 데 사용key 속성으로 index를 사용하는 것은 비추천React Hooksclass없이 state를 사용할 수 있는 기능Hooks를 사용하면 소스코드가 더 깔끔해진다. useEffect로 생명주기 통합할 수 있음리액트 불변성 지키기 참조 타입에서 객체나 배열의 값이 변할 떄 원본 데이터가 변경되기에 이 원본 데이터를 참조하고 있는 다른 객체에서 예상치 못한 오류가 발생할 수 있어서 프로그램 복잡도가 올라감리액트에서는 화면을 업데이트 할 때 불변성을 지켜서 값을 이전 값과 비교해서 변경하기 때문에 불변성을 지켜줘야함불변성 지키는 방법 : spread operator, map, filter, slice, reduce리액트 죄적화 렌더링이 필요없는 컴포넌트는 memo로 감싸주기useCallback 이용한 함수 최적화useMemo를 이용한 결과 값 최적화Tailwind CSS를 처음 사용해본 소감tailwind 사용은 이번이 처음이다. 이름만 알고 나중에 써봐야지 했던 라이브러리인데 이번에 사용해 볼 수 있어서 좋았다. tailwind는 많은 유틸리티 클래스를 조합해서 사용하기 때문에 클래스 명이 길어지고 그로 인해 html이 다소 복잡해 보인다. 또한 처음 클래스명을 익히는 데 시간이 걸린다고 생각한다. 하지만 css에 익숙하다면 tailwind 클래스명은 명확한 네이밍을 가지고 있어 클래스명만 봐도 어떤 스타일이 적용되는지 쉽게 이해할 수 있다. 처음만 낯설지 익숙해지는데 어렵지는 않은 것 같다. vsCode 확장도 잘 되어있어서 Tailwind CSS IntelliSense 를 사용하면 클래스 자동완성이 되고 설명도 볼 수 있어서 확인이 편리하다. 섹션 5~6 / Netflix 앱 만들기React Router DomRoutes : 앱에서 생성될 모든 개별 경로에 대한 컨테이너/상위 역할Route : 단일 경로를 만드는 데 사용 두가지 속성을 취합path는 원하는 컴포넌트의 url 경로 지정element 경로에 맞게 렌더링되어야 하는 컴포넌트 지정React Router Dom APIs중첩라우팅, 리액트 라우터의 강력한 기능 중 하나useNavigate / useParams / useLocationDebounce사용자가 미리 결정된 시간 동안 타이핑을 멈출 때까지 keyup이벤트의 처리를 지연 → 서버로 전송되는 api 호출 수가 줄어든다 입력된 모든 문자를 처리하면 성능이 저하되고 백엔드에 불필요한 로드가 추가될 수 있다. styled-components 사용clicked const Container = styled.div` background: red; `;이렇게 작성할 수 있다는 점이 편하다고 생각했다. 빌드 후는 모르겠지만 일단 작업할 때는 가독성이 좋다고 느꼈다. 컴포넌트 이름과 스타일이 한눈에 잘 보인다. props 활용 가능하다는 것도 좋았다. 간단한 프로젝트를 만들 때는 편하고 좋은 것 같은데 큰 프로젝트는 어떨지 모르겠다. 단점을 찾아봤는데 스타일과 컴포넌트 로직이 결합되어 분리가 어렵다는 단점이 있다고 한다. Tailwind / Styled-components는 가볍게 사용해 봤기 때문에 각각 어떤 장점, 단점이 있는지 검색해 봤다. Tailwind, Styled-components, SCSS 등 CSS 도구들은 각기 다른 장단점을 가지고 있기 때문에 프로젝트의 방향과 요구사항에 따라 적절한 결정을 하는 것도 중요할 것 같다. 2주차 과제이번에는 발자국이랑 과제 포스트를 분리해서 작성했다.비밀번호 생성하기 (Post Link)https://github.com/dpwl35/inflearn-warming-up-study/blob/main/06-password-generation/generator.js타이핑 테스트 앱 (Post Link)https://github.com/dpwl35/inflearn-warming-up-study/blob/main/07-typing-test/main.js 예산 계산기 앱(Post Link)https://github.com/dpwl35/inflearn-warming-up-study/tree/main/08-budget-calculato디즈니 플러스 앱(Post Link)https://github.com/dpwl35/inflearn-warming-up-study/tree/main/09-disneyplus
워밍업클럽
2024. 10. 06.
0
[인프런 워밍업 스터디 클럽 2기 FE] 2주차 과제 - 비밀번호 생성 앱
비밀번호 생성 앱 만들기GitHub : 06-password-generation 개요조건에 따른 비밀번호 생성 기능 구현Generator, Design Pattern 을 사용해보기 필요한 기능조건에 따른 비밀번호 생성생성된 비밀번호 복사 기능 구현하기//문자 범위 const charSets = { numbers: '0123456789', small: 'abcdefghijklmnopqrstuvwxyz', capital: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', symbols: '@!#$&%', }; //사용자 체크박스 설정 function getOptions() { return { numbers: document.getElementById("numbers").checked, small: document.getElementById("small").checked, capital: document.getElementById("capital").checked, symbols: document.getElementById("symbols").checked, }; }createButton.addEventListener("click", () => { const length = parseInt(inputLength.value, 10); const options = getOptions(); /*비밀번호 생성 최소 조건 생략 */ //Generator 사용할 때 const generatedPassword = generatePassword(length, options); //Factory Pattern 사용할 때 const generator = PasswordFactory.createPasswordGenerator(options); const generatedPassword = generator.generate(length); passwordElement.textContent = generatedPassword; }); 사용자가 선택한 비밀번호 길이와 옵션을 전달받아 비밀번호를 생성한다. 위의 설정들과 shufflePassword(password) 비밀번호 셔플 함수, copyToClipboard(text) 생성된 비밀번호 복사 함수는 같고 비밀번호 생성 과정만 다르다. Generator 사용function generatePassword(length, options) { const generator = passwordGenerator(options); let password = ""; // 선택된 옵션 배열 추가 const selectedSets = []; if (options.numbers) selectedSets.push(charSets.numbers); if (options.small) selectedSets.push(charSets.small); if (options.capital) selectedSets.push(charSets.capital); if (options.symbols) selectedSets.push(charSets.symbols); // 각 문자 집합에서 하나씩 선택하여 추가 selectedSets.forEach((set) => { password += set.charAt(Math.floor(Math.random() * set.length)); console.log(password); }); // 나머지 자리에 대해 랜덤 문자 추가 for (let i = password.length; i generatePassword(length, options)함수는 비밀번호를 생성하고 길이를 관리합니다.사용자 설정 옵션 반영:사용자가 선택한 옵션 값에 따라 selectedSets 배열을 만듭니다. 이 배열은 선택된 문자 집합(숫자, 소문자, 대문자, 기호)을 포함합니다. 최소 하나의 문자 포함:selectedSets 배열을 반복하여 각 문자 집합에서 최소한 하나의 문자를 포함시킵니다. 이는 비밀번호가 선택된 옵션을 만족하도록 보장합니다.남은 길이의 랜덤 문자 추가:나머지 비밀번호 길이에 대해서는 passwordGenerator를 사용하여 랜덤 문자를 생성합니다.비밀번호 반환: 최종적으로 생성된 비밀번호는 지정된 길이와 사용자 옵션을 반영하여 반환됩니다. function* passwordGenerator(options) { const selectedSets = []; if (options.numbers) selectedSets.push(charSets.numbers); if (options.small) selectedSets.push(charSets.small); if (options.capital) selectedSets.push(charSets.capital); if (options.symbols) selectedSets.push(charSets.symbols); while (true) { const randomSet = selectedSets[Math.floor(Math.random() * selectedSets.length)]; yield randomSet.charAt(Math.floor(Math.random() * randomSet.length)); } } passwordGenerator(options) 함수는 비밀번호를 생성합니다.무한 루프: while (true)를 사용하여 passwordGenerator()가 호출될 때 무한 루프를 실행합니다. 이 루프는 무작위 문자를 계속 생성하는 역할을 합니다.무작위 문자 집합 선택: 선택된 옵션 배열을 파라미터로 받아 randomSet에서 무작위로 하나의 문자 집합을 선택합니다. Math.random()을 사용하여 0과 1 사이의 무작위 소수를 생성한 후, 이를 selectedSets.length와 곱하여 배열의 인덱스를 얻습니다. Math.floor()를 사용하여 소수를 정수로 변환함으로써 유효한 인덱스 범위 내에서 선택이 이루어지도록 합니다.무작위 문자 선택: 위에서 선택된 문자 집합(randomSet)에서 문자를 선택합니다.예를 들어, 선택된 randomSet이 '0123456789'일 때, Math.random()이 0.65를 반환하면 0.65 * 10 (randomSet.length) = 6.5 → Math.floor()에 의해 6이 되고charAt(index) 메서드를 사용하여 문자열에서 지정된 인덱스에 있는 문자를 반환합니다. 랜덤 문자 생성 반복: 이 과정을 호출될 때마다 반복합니다. Factory Pattern 사용// 비밀번호 생성기 클래스 class PasswordGenerator { constructor(options) { this.options = options; this.selectedSets = []; if (options.numbers) this.selectedSets.push(charSets.numbers); if (options.small) this.selectedSets.push(charSets.small); if (options.capital) this.selectedSets.push(charSets.capital); if (options.symbols) this.selectedSets.push(charSets.symbols); } // 비밀번호 생성 generate(length) { let password = ""; this.selectedSets.forEach((set) => { password += set.charAt(Math.floor(Math.random() * set.length)); }); while (password.length PasswordFactory.createPasswordGenerator(options) 를 호출하여 PasswordGenerator 인스턴스를 생성한 후, 인스턴스의 generate(length) 메서드를 통해 비밀번호를 생성한다. 배운 걸 사용해보고자 Generator 함수와 클래스를 썼다.
워밍업클럽