부수효과(Side Effect)란? -javascript-

park.js·2025년 6월 9일
2

FrontEnd Develop log

목록 보기
44/44
post-thumbnail

예측가능한 코드, 상태관리와 밀접한 개념인 부수효과(Side Effect)에 대해 알아보자.

부수효과(Side Effect)란?

부수효과는 함수가 실행될 때 함수 외부의 상태를 변경하거나, 외부 상태에 의존하는 모든 행위를 말한다.

부수효과에서 언급하는 '외부'는 함수의 스코프 밖에 있는 모든 것을 의미한다. 하.. 스코프는 또 뭐지? => 클릭

1. 예시로 이해해보자

// 🚨 부수효과가 있는 함수들

// 1. 외부 변수 변경
let total = 0;
const addToTotal = (num: number) => {
  total += num;  // 외부 변수 total을 변경
};

// 2. 콘솔 출력
const logUser = (user: User) => {
  console.log(user);  // 외부 환경(콘솔)에 출력
};

// 3. API 호출
const saveUser = async (user: User) => {
  await fetch('/api/users', {  // 외부 서버와 통신
    method: 'POST',
    body: JSON.stringify(user)
  });
};

// 4. DOM 조작
const updateUI = (element: HTMLElement) => {
  element.textContent = 'Updated!';  // DOM 상태 변경
};

2. 순수 함수(Pure Function)의 예시

// ✅ 부수효과가 없는 순수 함수들

// 1. 입력값에 따른 출력값만 반환
const double = (num: number): number => {
  return num * 2;
};

// 2. 객체 변환
const formatUser = (user: User): FormattedUser => {
  return {
    fullName: `${user.firstName} ${user.lastName}`,
    age: calculateAge(user.birthDate)
  };
};

// 3. 배열 변환
const filterActiveUsers = (users: User[]): User[] => {
  return users.filter(user => user.isActive);
};

3. 부수효과의 특징

  1. 예측 불가능성
    • 외부 상태에 의존하므로 결과를 예측하기 어려움
    • 테스트가 복잡해짐
// 🚨 예측 불가능성 예시
let globalCount = 0;

const increment = () => {
  globalCount++;
  return globalCount;
};

console.log(increment()); // 1
console.log(increment()); // 2
// 다른 개발자가 globalCount를 100으로 변경했다면?
console.log(increment()); // 101 (예측 불가능!)

// ✅ 예측 가능한 코드
const increment = (count: number) => {
  return count + 1;
};

console.log(increment(0)); // 1
console.log(increment(1)); // 2
console.log(increment(2)); // 3 (항상 예측 가능!)
  1. 디버깅의 어려움
    • 문제 발생 시 원인 파악이 어려움
    • 상태 변경의 흐름을 추적하기 어려움
// 🚨 디버깅이 어려운 코드예시

// 1. 첫 번째 코드 (부수효과 있음)
let user = { name: '김지성' };

const updateUser = () => {
 user.name = '박지성';  // 🚨 외부 변수 직접 수정
 console.log('User updated');
};

updateUser();
console.log(user); // { name: '박지성' }

// 2. 두 번째 코드 (순수 함수)
const user = { name: '김지성' };

const updateUser = (user: User) => {
 return { ...user, name: '박지성' };  // ✅ 새로운 객체 반환
};

const newUser = updateUser(user);
console.log(user);     // { name: '김지성' } (원본 유지)
console.log(newUser);  // { name: '박지성' } (새로운 객체)

주요 차이점:

1. 원본 데이터 변경 여부
  - 첫 번째: 원본 `user` 객체를 직접 수정
  - 두 번째: 원본은 그대로 두고 새로운 객체를 반환

2. 함수의 예측 가능성
  - 첫 번째: 함수 호출 시 외부 변수 `user`의 상태에 따라 결과가 달라짐
  - 두 번째: 입력값이 같으면 항상 같은 결과를 반환

3. 디버깅 용이성
  - 첫 번째: `user.name`이 어디서 변경되었는지 추적이 어려움
  - 두 번째: 상태 변경이 명확하게 함수의 반환값으로 표현됨

4. 사용 예시
// 첫 번째 방식
let user = { name: '김지성' };
updateUser();
// user가 변경되었는지 확인하려면 user를 직접 확인해야 함

// 두 번째 방식
const user = { name: '박지성' };
const updatedUser = updateUser(user);
// 변경된 내용이 명확하게 updatedUser에 담겨있음

두번째 방식 특히 React의 불변성(Immutability) 원칙에 맞다. 
불변성(Immutability): 데이터가 한번 생성되면 그 값을 변경할 수 없고, 대신 새로운 데이터를 생성하여 사용해야 한다는 원칙.
React에서 불변성을 지키면 상태 변경의 추적이 용이해짐.
예측 가능한 애플리케이션을 만들 수 있다.
  1. 코드 재사용성 저하
    • 외부 상태에 의존하므로 다른 환경에서 재사용이 어려움

4. 부수효과를 최소화하는 방법

// 🚨 부수효과가 많은 코드
class UserManager {
  private users: User[] = [];
  
  addUser(user: User) {
    this.users.push(user);
    console.log(`User added: ${user.name}`);
    this.saveToDatabase(user);
    this.updateUI();
  }
}

// ✅ 부수효과를 분리한 코드
class UserManager {
  private users: User[] = [];
  
  // 순수 함수: 데이터 변환만 담당
  addUser(user: User): User[] {
    return [...this.users, user];
  }
  
  // 부수효과를 명시적으로 분리
  async handleUserAddition(user: User) {
    const newUsers = this.addUser(user);
    await this.saveToDatabase(user);
    this.notifyUserAdded(user);
    this.updateUI(newUsers);
  }
}

5. React에서의 부수효과 관리

무한 렌더링 가능성

const UserProfile: React.FC = () => {
  const [user, setUser] = useState<User | null>(null);
  
  // 이 함수는 컴포넌트가 렌더링될 때마다 새로 생성됨
  const fetchUser = async () => {
    const response = await fetch('/api/user');
    const data = await response.json();
    setUser(data);  // setUser 호출 → 상태 변경 → 컴포넌트 재렌더링
  };
  
  return <div>{user?.name}</div>;
};

문제점:

  • fetchUser 함수가 컴포넌트 내부에 있어서, 컴포넌트가 렌더링될 때마다 새로운 함수가 생성됨
  • 이 함수가 setUser를 호출하면 상태가 변경되어 컴포넌트가 다시 렌더링됨
  • 다시 렌더링되면 fetchUser가 다시 생성되고... 이렇게 무한 루프가 발생할 수 있음

데이터 페칭 시점 불명확

const UserProfile: React.FC = () => {
  const [user, setUser] = useState<User | null>(null);
  
  const fetchUser = async () => {
    const response = await fetch('/api/user');
    const data = await response.json();
    setUser(data);
  };
  
  // fetchUser를 어디서 호출해야 할지 명확하지 않음
  return <div>{user?.name}</div>;
};

문제점:

  • fetchUser 함수는 정의만 되어있고, 어디서 호출해야 할지가 명확하지 않음
  • 컴포넌트가 처음 마운트될 때 자동으로 데이터를 가져오지 않음
  • 사용자가 직접 호출해야 하는지, 자동으로 호출되어야 하는지 불명확

부수효과와 렌더링 로직이 섞여있음

const UserProfile: React.FC = () => {
  const [user, setUser] = useState<User | null>(null);
  
  // 데이터 가져오기 (부수효과)
  const fetchUser = async () => {
    const response = await fetch('/api/user');
    const data = await response.json();
    setUser(data);
  };
  
  // UI 그리기 (렌더링 로직)
  return <div>{user?.name}</div>;
};

문제점:

  • 데이터를 가져오는 로직(fetchUser)과 UI를 그리는 로직(return)이 한 컴포넌트에 섞여있음
  • 이렇게 되면 코드가 복잡해지고 유지보수가 어려워짐

개선된 버전

const UserProfile: React.FC = () => {
  const [user, setUser] = useState<User | null>(null);
  
  // useEffect로 데이터 가져오기 로직을 분리
  useEffect(() => {
    const fetchUser = async () => {
      const response = await fetch('/api/user');
      const data = await response.json();
      setUser(data);
    };
    
    fetchUser();  // 컴포넌트 마운트 시 한 번만 실행
  }, []);  // 빈 배열: 의존성이 없으므로 마운트 시 한 번만 실행
  
  // UI 그리기 로직만 남음
  return <div>{user?.name}</div>;
};

장점:
1. useEffect를 사용해 데이터 가져오기 로직을 분리
2. 컴포넌트 마운트 시 한 번만 실행되도록 명확하게 지정
3. UI 그리기 로직만 남아서 코드가 깔끔해짐

이렇게 하면 코드가 더 예측 가능하고 유지보수하기 쉬워짐

결론

이 글을 여기까지 훑고 부수효과가 무엇이고, 프로그램에 어떤 영향을 미칠 수 있는지 정도 이해했다면 성공적이라고 생각한다.
부수효과는 코드의 예측 가능성과 유지보수성을 저하시키는 주요 원인이다. 하지만 현실적인 프로그래밍에서 부수효과를 완전히 제거하기는 불가능에 가깝다.

따라서 이 글의 핵심은 부수효과의 제거가 아닌 '이 함수를 호출하면 외부에 어떤 영향을 줄지 코드만 봐도 알 수 있게 하자'이다.

// ❌ 숨겨진 부수효과
const addToCart = (item) => {
  const newItem = { ...item, id: Date.now() };
  cart.push(newItem); // 🚨 전역 배열 변경 (숨겨진 부수효과)
  return newItem;
};

// ✅ 명시적 부수효과
const createCartItem = (item) => {
  return { ...item, id: Date.now() }; // 순수 함수
};

const addToCart = (item) => {
  const newItem = createCartItem(item);
  cart.push(newItem); // 명시적으로 전역 상태 변경
  return newItem;
};
profile
참 되게 살자

0개의 댓글